""" 灵感点分析结果可视化脚本 读取 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 if "文件路径" in data: step1_path = data["文件路径"].get("step1") step2_path = data["文件路径"].get("step2") if step1_path: step1_full_path = Path(step1_path) if not step1_full_path.is_absolute(): step1_full_path = inspiration_path.parent.parent.parent.parent / step1_path if step1_full_path.exists(): with open(step1_full_path, 'r', encoding='utf-8') as f: step1_data = json.load(f) if step2_path: step2_full_path = Path(step2_path) if not step2_full_path.is_absolute(): step2_full_path = inspiration_path.parent.parent.parent.parent / step2_path if step2_full_path.exists(): with open(step2_full_path, 'r', encoding='utf-8') as f: step2_data = json.load(f) 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] element_name = top_match.get("要素", {}).get("名称", "") match_score = top_match.get("分数", 0) step1_match_preview = f'''
🎯 Step1 Top1匹配
{html_module.escape(element_name)} {match_score:.2f}
''' # 获取Step2匹配结果(简要展示) step2_matches = step2.get("匹配结果", []) if step2 else [] step2_match_preview = "" if step2_matches: top_match = step2_matches[0] words = top_match.get("增量词", []) match_score = top_match.get("分数", 0) step2_match_preview = f'''
➕ Step2 Top1增量词
{html_module.escape(", ".join(words))} {match_score:.2f}
''' # 准备详细数据用于弹窗 detail_data_json = json.dumps(inspiration_data, ensure_ascii=False) detail_data_json_escaped = html_module.escape(detail_data_json) 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_modal_content_js() -> str: """ 生成详情弹窗内容的JavaScript函数 Returns: JavaScript代码字符串 """ return ''' function showInspirationDetail(element) { const inspirationName = element.dataset.inspirationName; const detailStr = element.dataset.detail; let detail; try { detail = JSON.parse(detailStr); } catch(e) { console.error('解析数据失败:', e); return; } const modal = document.getElementById('detailModal'); const modalBody = document.getElementById('modalBody'); const summary = detail.summary || {}; const step1 = detail.step1 || {}; const step2 = detail.step2 || {}; const metrics = summary.关键指标 || {}; // 构建Modal内容 let content = ` `; // 元数据信息 const metadata = summary.元数据 || {}; if (metadata.current_time || metadata.流程) { content += ` `; } // 关键指标 content += ` `; // Step1 详细信息 if (step1 && step1.灵感) { const inspiration = step1.灵感 || ''; const persona = step1.人设 || {}; const matches = step1.匹配结果 || []; content += ` `; } // Step2 详细信息 if (step2 && step2.灵感) { const step2Matches = step2.匹配结果 || []; content += ` `; } // 日志链接 if (metadata.log_url) { content += ` `; } modalBody.innerHTML = content; 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 === 'score-desc' || sortSelect === 'score-asc') { visibleCards.sort((a, b) => { const detailA = JSON.parse(a.dataset.detail); const detailB = JSON.parse(b.dataset.detail); const scoreA = ((detailA.summary.关键指标.step1_top1_score || 0) + (detailA.summary.关键指标.step2_score || 0)) / 2; const scoreB = ((detailB.summary.关键指标.step1_top1_score || 0) + (detailB.summary.关键指标.step2_score || 0)) / 2; return sortSelect === 'score-desc' ? scoreB - scoreA : scoreA - scoreB; }); } 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 excellentCount = 0; let goodCount = 0; let normalCount = 0; let needOptCount = 0; let totalScore = 0; visibleCards.forEach(card => { const detail = JSON.parse(card.dataset.detail); const metrics = detail.summary.关键指标; const score = ((metrics.step1_top1_score || 0) + (metrics.step2_score || 0)) / 2 * 100; totalScore += score; if (score >= 70) excellentCount++; else if (score >= 50) goodCount++; else if (score >= 30) normalCount++; else needOptCount++; }); document.getElementById('excellentCount').textContent = excellentCount; document.getElementById('goodCount').textContent = goodCount; document.getElementById('normalCount').textContent = normalCount; document.getElementById('needOptCount').textContent = needOptCount; const avgScore = visibleCards.length > 0 ? (totalScore / visibleCards.length).toFixed(1) : 0; document.getElementById('avgScore').textContent = avgScore; } ''' def generate_html( inspirations_data: List[Dict[str, Any]], posts_map: Dict[str, Dict[str, Any]], output_path: str ) -> str: """ 生成完整的可视化HTML Args: inspirations_data: 灵感点数据列表 posts_map: 帖子数据映射 output_path: 输出文件路径 Returns: 输出文件路径 """ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 统计信息 total_count = len(inspirations_data) excellent_count = sum(1 for d in inspirations_data if ((d["summary"].get("关键指标", {}).get("step1_top1_score", 0) + d["summary"].get("关键指标", {}).get("step2_score", 0)) / 2 * 100) >= 70) good_count = sum(1 for d in inspirations_data if 50 <= ((d["summary"].get("关键指标", {}).get("step1_top1_score", 0) + d["summary"].get("关键指标", {}).get("step2_score", 0)) / 2 * 100) < 70) normal_count = sum(1 for d in inspirations_data if 30 <= ((d["summary"].get("关键指标", {}).get("step1_top1_score", 0) + d["summary"].get("关键指标", {}).get("step2_score", 0)) / 2 * 100) < 50) need_opt_count = sum(1 for d in inspirations_data if ((d["summary"].get("关键指标", {}).get("step1_top1_score", 0) + d["summary"].get("关键指标", {}).get("step2_score", 0)) / 2 * 100) < 30) total_score = sum((d["summary"].get("关键指标", {}).get("step1_top1_score", 0) + d["summary"].get("关键指标", {}).get("step2_score", 0)) / 2 * 100 for d in inspirations_data) avg_score = total_score / total_count if total_count > 0 else 0 # 按综合分数排序 inspirations_data_sorted = sorted( inspirations_data, key=lambda x: (x["summary"].get("关键指标", {}).get("step1_top1_score", 0) + x["summary"].get("关键指标", {}).get("step2_score", 0)) / 2, reverse=True ) # 生成卡片HTML cards_html = [generate_inspiration_card_html(data) for data in inspirations_data_sorted] cards_html_str = '\n'.join(cards_html) # 生成JavaScript detail_modal_js = generate_detail_modal_content_js() # 完整HTML html_content = f''' 灵感点分析可视化

💡 灵感点分析可视化

基于HOW人设的灵感点匹配分析结果
分析总数
{total_count}
优秀 (≥70)
{excellent_count}
良好 (50-70)
{good_count}
一般 (30-50)
{normal_count}
待优化 (<30)
{need_opt_count}
平均分数
{avg_score:.1f}
排序方式:
{cards_html_str}
生成时间: {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 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/阿里多多酱/作者历史帖子" 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)} 个帖子") # 生成HTML print("\n🎨 正在生成可视化HTML...") result_path = generate_html(inspirations_data, posts_map, output_path) print(f"\n✅ 可视化文件已生成!") print(f"📄 文件路径: {result_path}") print(f"\n💡 在浏览器中打开该文件即可查看可视化结果") print("=" * 60) if __name__ == "__main__": main()