""" 灵感点分析结果可视化脚本 读取 how/灵感点 目录下的分析结果,结合作者历史帖子详情,生成可视化HTML页面 """ import json from pathlib import Path from typing import Dict, Any, List, Optional from datetime import datetime import html as html_module def load_inspiration_points_data(inspiration_dir: str) -> List[Dict[str, Any]]: """ 加载所有灵感点的分析结果 Args: inspiration_dir: 灵感点目录路径 Returns: 灵感点分析结果列表 """ inspiration_path = Path(inspiration_dir) results = [] # 遍历所有子目录 for subdir in inspiration_path.iterdir(): if subdir.is_dir(): # 查找 all_summary 文件 summary_files = list(subdir.glob("all_summary_*.json")) if summary_files: summary_file = summary_files[0] try: with open(summary_file, 'r', encoding='utf-8') as f: data = json.load(f) # 加载完整的 step1 和 step2 数据 step1_data = None step2_data = None # 直接从当前子目录查找 step1 和 step2 文件 step1_files = list(subdir.glob("all_step1_*.json")) step2_files = list(subdir.glob("all_step2_*.json")) if step1_files: try: with open(step1_files[0], 'r', encoding='utf-8') as f: step1_data = json.load(f) except Exception as e: print(f"警告: 读取 {step1_files[0]} 失败: {e}") if step2_files: try: with open(step2_files[0], 'r', encoding='utf-8') as f: step2_data = json.load(f) except Exception as e: print(f"警告: 读取 {step2_files[0]} 失败: {e}") results.append({ "summary": data, "step1": step1_data, "step2": step2_data, "inspiration_name": subdir.name }) except Exception as e: print(f"警告: 读取 {summary_file} 失败: {e}") return results def load_posts_data(posts_dir: str) -> Dict[str, Dict[str, Any]]: """ 加载所有帖子详情数据 Args: posts_dir: 帖子目录路径 Returns: 帖子ID到帖子详情的映射 """ posts_path = Path(posts_dir) posts_map = {} for post_file in posts_path.glob("*.json"): try: with open(post_file, 'r', encoding='utf-8') as f: post_data = json.load(f) post_id = post_data.get("channel_content_id") if post_id: posts_map[post_id] = post_data except Exception as e: print(f"警告: 读取 {post_file} 失败: {e}") return posts_map def generate_inspiration_card_html(inspiration_data: Dict[str, Any]) -> str: """ 生成单个灵感点的卡片HTML Args: inspiration_data: 灵感点数据 Returns: HTML字符串 """ summary = inspiration_data.get("summary", {}) step1 = inspiration_data.get("step1", {}) step2 = inspiration_data.get("step2", {}) inspiration_name = inspiration_data.get("inspiration_name", "未知灵感") # 提取关键指标 metrics = summary.get("关键指标", {}) step1_score = metrics.get("step1_top1_score", 0) step2_score = metrics.get("step2_score", 0) step1_match_element = metrics.get("step1_top1_匹配要素", "") step2_increment_count = metrics.get("step2_增量词数量", 0) # 确定卡片颜色(基于Step1分数) if step1_score >= 0.7: border_color = "#10b981" step1_color = "#10b981" elif step1_score >= 0.5: border_color = "#f59e0b" step1_color = "#f59e0b" elif step1_score >= 0.3: border_color = "#3b82f6" step1_color = "#3b82f6" else: border_color = "#ef4444" step1_color = "#ef4444" # Step2颜色 if step2_score >= 0.7: step2_color = "#10b981" elif step2_score >= 0.5: step2_color = "#f59e0b" elif step2_score >= 0.3: step2_color = "#3b82f6" else: step2_color = "#ef4444" # 转义HTML inspiration_name_escaped = html_module.escape(inspiration_name) step1_match_element_escaped = html_module.escape(step1_match_element) # 获取Step1匹配结果(简要展示) step1_matches = step1.get("匹配结果列表", []) if step1 else [] step1_match_preview = "" if step1_matches: top_match = step1_matches[0] # 从新的数据结构中提取信息 input_info = top_match.get("输入信息", {}) match_result = top_match.get("匹配结果", {}) element_name = input_info.get("A", "") match_score = match_result.get("score", 0) same_parts = match_result.get("相同部分", {}) increment_parts = match_result.get("增量部分", {}) # 生成相同部分和增量部分的HTML parts_html = "" if same_parts: same_items = [f"{html_module.escape(k)}" for k in same_parts.keys()] parts_html += f'
相同: {", ".join(same_items)}
' if increment_parts: inc_items = [f"{html_module.escape(k)}" for k in increment_parts.keys()] parts_html += f'
增量: {", ".join(inc_items)}
' step1_match_preview = f'''
🎯 Step1 Top1匹配
{html_module.escape(element_name)} {match_score:.2f}
{parts_html}
''' # 获取Step2匹配结果(简要展示) step2_match_preview = "" if step2: input_info = step2.get("输入信息", {}) match_result = step2.get("匹配结果", {}) increment_word = input_info.get("B", "") match_score = match_result.get("score", 0) same_parts = match_result.get("相同部分", {}) increment_parts = match_result.get("增量部分", {}) # 只有当增量词不为空时才显示 if increment_word.strip(): # 生成相同部分和增量部分的HTML parts_html = "" if same_parts: same_items = [f"{html_module.escape(k)}" for k in same_parts.keys()] parts_html += f'
相同: {", ".join(same_items)}
' if increment_parts: inc_items = [f"{html_module.escape(k)}" for k in increment_parts.keys()] parts_html += f'
增量: {", ".join(inc_items)}
' step2_match_preview = f'''
➕ Step2 Top1增量词
{html_module.escape(increment_word)} {match_score:.2f}
{parts_html}
''' # 准备详细数据用于弹窗 detail_data_json = json.dumps(inspiration_data, ensure_ascii=False) detail_data_json_escaped = html_module.escape(detail_data_json) # 生成详细HTML并进行HTML转义 detail_html = generate_detail_html(inspiration_data) detail_html_escaped = html_module.escape(detail_html) html = f'''

{inspiration_name_escaped}

Step1分数
{step1_score:.3f}
Step2分数
{step2_score:.3f}
{step1_match_preview} {step2_match_preview}
📊 增量词数: {step2_increment_count}
点击查看详情 →
''' return html def generate_detail_html(inspiration_data: Dict[str, Any]) -> str: """ 生成灵感点的详细信息HTML Args: inspiration_data: 灵感点数据 Returns: 详细信息的HTML字符串 """ import html as html_module summary = inspiration_data.get("summary", {}) step1 = inspiration_data.get("step1", {}) step2 = inspiration_data.get("step2", {}) inspiration_name = inspiration_data.get("inspiration_name", "未知灵感") content = f''' ''' # 获取元数据,用于后面的日志链接 metadata = summary.get("元数据", {}) # Step1 详细信息 if step1 and step1.get("灵感"): inspiration = step1.get("灵感", "") matches = step1.get("匹配结果列表", []) content += f''' ''' # Step2 详细信息 if step2 and step2.get("灵感"): input_info = step2.get("输入信息", {}) match_result = step2.get("匹配结果", {}) increment_word = input_info.get("B", "") b_context = input_info.get("B_Context", "") score = match_result.get("score", 0) score_explain = match_result.get("score说明", "") same_parts = match_result.get("相同部分", {}) increment_parts = match_result.get("增量部分", {}) content += ''' ''' # 日志链接 if metadata.get("log_url"): content += f''' ''' return content def generate_detail_modal_content_js() -> str: """ 生成详情弹窗内容的JavaScript函数 Returns: JavaScript代码字符串 """ return ''' // Tab切换功能 function switchTab(event, tabId) { // 移除所有tab的active状态 const tabButtons = document.querySelectorAll('.tab-button'); tabButtons.forEach(button => { button.classList.remove('active'); }); // 隐藏所有tab内容 const tabContents = document.querySelectorAll('.tab-content'); tabContents.forEach(content => { content.classList.remove('active'); }); // 激活当前tab event.currentTarget.classList.add('active'); document.getElementById(tabId).classList.add('active'); } function showInspirationDetail(element) { const detailHtml = element.dataset.detailHtml; const modal = document.getElementById('detailModal'); const modalBody = document.getElementById('modalBody'); modalBody.innerHTML = detailHtml; modal.classList.add('active'); document.body.style.overflow = 'hidden'; } function closeModal() { const modal = document.getElementById('detailModal'); modal.classList.remove('active'); document.body.style.overflow = ''; } function closeModalOnOverlay(event) { if (event.target.id === 'detailModal') { closeModal(); } } // ESC键关闭Modal document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { closeModal(); } }); // 搜索和过滤功能 function filterInspirations() { const searchInput = document.getElementById('searchInput').value.toLowerCase(); const sortSelect = document.getElementById('sortSelect').value; const cards = document.querySelectorAll('.inspiration-card'); let visibleCards = Array.from(cards); // 搜索过滤 visibleCards.forEach(card => { const name = card.dataset.inspirationName.toLowerCase(); if (name.includes(searchInput)) { card.style.display = ''; } else { card.style.display = 'none'; } }); // 获取可见的卡片 visibleCards = Array.from(cards).filter(card => card.style.display !== 'none'); // 排序 if (sortSelect === 'step1-desc' || sortSelect === 'step1-asc') { visibleCards.sort((a, b) => { const step1A = parseFloat(a.dataset.step1Score) || 0; const step1B = parseFloat(b.dataset.step1Score) || 0; const step2A = parseFloat(a.dataset.step2Score) || 0; const step2B = parseFloat(b.dataset.step2Score) || 0; if (sortSelect === 'step1-desc') { return step1B !== step1A ? step1B - step1A : step2B - step2A; } else { return step1A !== step1B ? step1A - step1B : step2A - step2B; } }); } else if (sortSelect === 'step2-desc' || sortSelect === 'step2-asc') { visibleCards.sort((a, b) => { const step2A = parseFloat(a.dataset.step2Score) || 0; const step2B = parseFloat(b.dataset.step2Score) || 0; const step1A = parseFloat(a.dataset.step1Score) || 0; const step1B = parseFloat(b.dataset.step1Score) || 0; if (sortSelect === 'step2-desc') { return step2B !== step2A ? step2B - step2A : step1B - step1A; } else { return step2A !== step2B ? step2A - step2B : step1A - step1B; } }); } else if (sortSelect === 'name-asc' || sortSelect === 'name-desc') { visibleCards.sort((a, b) => { const nameA = a.dataset.inspirationName; const nameB = b.dataset.inspirationName; return sortSelect === 'name-asc' ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA); }); } // 重新排列卡片 const container = document.querySelector('.inspirations-grid'); visibleCards.forEach(card => { container.appendChild(card); }); // 更新统计 updateStats(); } function updateStats() { const cards = document.querySelectorAll('.inspiration-card'); const visibleCards = Array.from(cards).filter(card => card.style.display !== 'none'); document.getElementById('totalCount').textContent = visibleCards.length; let step1ExcellentCount = 0; let step1GoodCount = 0; let step1NormalCount = 0; let step1NeedOptCount = 0; let step2ExcellentCount = 0; let step2GoodCount = 0; let step2NormalCount = 0; let step2NeedOptCount = 0; let totalStep1Score = 0; let totalStep2Score = 0; visibleCards.forEach(card => { const step1Score = parseFloat(card.dataset.step1Score) || 0; const step2Score = parseFloat(card.dataset.step2Score) || 0; totalStep1Score += step1Score; totalStep2Score += step2Score; // Step1 统计 if (step1Score >= 0.7) step1ExcellentCount++; else if (step1Score >= 0.5) step1GoodCount++; else if (step1Score >= 0.3) step1NormalCount++; else step1NeedOptCount++; // Step2 统计 if (step2Score >= 0.7) step2ExcellentCount++; else if (step2Score >= 0.5) step2GoodCount++; else if (step2Score >= 0.3) step2NormalCount++; else step2NeedOptCount++; }); document.getElementById('step1ExcellentCount').textContent = step1ExcellentCount; document.getElementById('step1GoodCount').textContent = step1GoodCount; document.getElementById('step1NormalCount').textContent = step1NormalCount; document.getElementById('step1NeedOptCount').textContent = step1NeedOptCount; document.getElementById('step2ExcellentCount').textContent = step2ExcellentCount; document.getElementById('step2GoodCount').textContent = step2GoodCount; document.getElementById('step2NormalCount').textContent = step2NormalCount; document.getElementById('step2NeedOptCount').textContent = step2NeedOptCount; const avgStep1Score = visibleCards.length > 0 ? (totalStep1Score / visibleCards.length).toFixed(3) : '0.000'; const avgStep2Score = visibleCards.length > 0 ? (totalStep2Score / visibleCards.length).toFixed(3) : '0.000'; document.getElementById('avgStep1Score').textContent = avgStep1Score; document.getElementById('avgStep2Score').textContent = avgStep2Score; } ''' def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str: """ 生成人设结构的树状HTML Args: persona_data: 人设数据 Returns: 人设结构的HTML字符串 """ if not persona_data: return '
暂无人设数据
' inspiration_list = persona_data.get("灵感点列表", []) if not inspiration_list: return '
暂无灵感点列表数据
' html_parts = ['
'] for perspective_idx, perspective in enumerate(inspiration_list): perspective_name = perspective.get("视角名称", "未知视角") perspective_desc = perspective.get("视角描述", "") pattern_list = perspective.get("模式列表", []) # 一级节点:视角 html_parts.append(f''' ') html_parts.append('
') return ''.join(html_parts) def generate_html( inspirations_data: List[Dict[str, Any]], posts_map: Dict[str, Dict[str, Any]], persona_data: Dict[str, Any], output_path: str ) -> str: """ 生成完整的可视化HTML Args: inspirations_data: 灵感点数据列表 posts_map: 帖子数据映射 persona_data: 人设数据 output_path: 输出文件路径 Returns: 输出文件路径 """ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 统计信息 total_count = len(inspirations_data) # Step1 统计 step1_excellent_count = sum(1 for d in inspirations_data if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) >= 0.7) step1_good_count = sum(1 for d in inspirations_data if 0.5 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.7) step1_normal_count = sum(1 for d in inspirations_data if 0.3 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.5) step1_need_opt_count = sum(1 for d in inspirations_data if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.3) # Step2 统计 step2_excellent_count = sum(1 for d in inspirations_data if d["summary"].get("关键指标", {}).get("step2_score", 0) >= 0.7) step2_good_count = sum(1 for d in inspirations_data if 0.5 <= d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.7) step2_normal_count = sum(1 for d in inspirations_data if 0.3 <= d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.5) step2_need_opt_count = sum(1 for d in inspirations_data if d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.3) # 平均分数 total_step1_score = sum(d["summary"].get("关键指标", {}).get("step1_top1_score", 0) for d in inspirations_data) total_step2_score = sum(d["summary"].get("关键指标", {}).get("step2_score", 0) for d in inspirations_data) avg_step1_score = total_step1_score / total_count if total_count > 0 else 0 avg_step2_score = total_step2_score / total_count if total_count > 0 else 0 # 按Step1分数排序(Step2作为次要排序) inspirations_data_sorted = sorted( inspirations_data, key=lambda x: (x["summary"].get("关键指标", {}).get("step1_top1_score", 0), x["summary"].get("关键指标", {}).get("step2_score", 0)), reverse=True ) # 生成卡片HTML cards_html = [generate_inspiration_card_html(data) for data in inspirations_data_sorted] cards_html_str = '\n'.join(cards_html) # 生成人设结构HTML persona_structure_html = generate_persona_structure_html(persona_data) # 生成JavaScript detail_modal_js = generate_detail_modal_content_js() # 完整HTML html_content = f''' 灵感点分析可视化

💡 灵感点分析可视化

基于HOW人设的灵感点匹配分析结果
分析总数
{total_count}
Step1优秀 (≥0.7)
{step1_excellent_count}
Step1良好 (0.5-0.7)
{step1_good_count}
Step1一般 (0.3-0.5)
{step1_normal_count}
Step1待优化 (<0.3)
{step1_need_opt_count}
Step2优秀 (≥0.7)
{step2_excellent_count}
Step2良好 (0.5-0.7)
{step2_good_count}
Step2一般 (0.3-0.5)
{step2_normal_count}
Step2待优化 (<0.3)
{step2_need_opt_count}
Step1平均分
{avg_step1_score:.3f}
Step2平均分
{avg_step2_score:.3f}
排序方式:
{cards_html_str}

📚 人设结构

{persona_structure_html}
生成时间: {timestamp}
''' # 写入文件 output_file = Path(output_path) output_file.parent.mkdir(parents=True, exist_ok=True) with open(output_file, 'w', encoding='utf-8') as f: f.write(html_content) return str(output_file.absolute()) def load_persona_data(persona_path: str) -> Dict[str, Any]: """ 加载人设数据 Args: persona_path: 人设JSON文件路径 Returns: 人设数据字典 """ try: with open(persona_path, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: print(f"警告: 读取人设文件失败: {e}") return {} def main(): """主函数""" import sys # 配置路径 inspiration_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点" posts_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/作者历史帖子" persona_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/人设.json" output_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点可视化.html" print("=" * 60) print("灵感点分析可视化脚本") print("=" * 60) # 加载数据 print("\n📂 正在加载灵感点数据...") inspirations_data = load_inspiration_points_data(inspiration_dir) print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点") print("\n📂 正在加载帖子数据...") posts_map = load_posts_data(posts_dir) print(f"✅ 成功加载 {len(posts_map)} 个帖子") print("\n📂 正在加载人设数据...") persona_data = load_persona_data(persona_path) print(f"✅ 成功加载人设数据") # 生成HTML print("\n🎨 正在生成可视化HTML...") result_path = generate_html(inspirations_data, posts_map, persona_data, output_path) print(f"\n✅ 可视化文件已生成!") print(f"📄 文件路径: {result_path}") print(f"\n💡 在浏览器中打开该文件即可查看可视化结果") print("=" * 60) if __name__ == "__main__": main()