""" 灵感点分析结果可视化脚本 读取 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]]: """ 加载所有灵感点的分析结果(包含 step1 和 step3) Args: inspiration_dir: 灵感点目录路径 Returns: 灵感点分析结果列表 """ inspiration_path = Path(inspiration_dir) results = [] # 遍历所有子目录 for subdir in inspiration_path.iterdir(): if subdir.is_dir(): # 查找 step1 文件 step1_files = list(subdir.glob("all_step1_*.json")) # 查找 step3 文件 step3_files = list(subdir.glob("all_step3_*.json")) if step1_files: try: # 读取 step1 with open(step1_files[0], 'r', encoding='utf-8') as f: step1_data = json.load(f) # 尝试读取 step3 step3_data = None if step3_files: try: with open(step3_files[0], 'r', encoding='utf-8') as f: step3_data = json.load(f) except Exception as e: print(f"警告: 读取 {step3_files[0]} 失败: {e}") results.append({ "step1": step1_data, "step3": step3_data, "inspiration_name": subdir.name }) except Exception as e: print(f"警告: 读取 {step1_files[0]} 失败: {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字符串 """ step1 = inspiration_data.get("step1", {}) inspiration_name = inspiration_data.get("inspiration_name", "未知灵感") # 从step1中获取top1匹配分数 step1_matches = step1.get("匹配结果列表", []) if step1 else [] step1_score = 0 step1_match_element = "" if step1_matches: top_match = step1_matches[0] match_result = top_match.get("匹配结果", {}) step1_score = match_result.get("score", 0) input_info = top_match.get("输入信息", {}) step1_match_element = input_info.get("A_名称", "") # 确定卡片颜色(基于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" # 转义HTML inspiration_name_escaped = html_module.escape(inspiration_name) step1_match_element_escaped = html_module.escape(step1_match_element) # 获取Step1匹配结果(简要展示Top3) step1_match_preview = "" if step1_matches: preview_items = [] for idx, match in enumerate(step1_matches[:3]): input_info = match.get("输入信息", {}) match_result = match.get("匹配结果", {}) element_name = input_info.get("A_名称", "") match_score = match_result.get("score", 0) # 确定排名标记 rank_emoji = ["🥇", "🥈", "🥉"][idx] preview_items.append(f'''
{rank_emoji} {html_module.escape(element_name)} {match_score:.2f}
''') step1_match_preview = f'''
🎯 Top3 匹配要素
{"".join(preview_items)}
''' # 获取 Step3 生成的灵感点(简要展示) step3_preview = "" step3 = inspiration_data.get("step3") if step3: step3_inspirations = step3.get("灵感点列表", []) if step3_inspirations: preview_items = [] for idx, item in enumerate(step3_inspirations[:3]): path = item.get("推理路径", "") insp_point = item.get("灵感点", "") preview_items.append(f'''
💡 {html_module.escape(insp_point)} {html_module.escape(path)}
''') step3_preview = f'''
✨ Step3 生成的灵感点 (前3个,共{len(step3_inspirations)}个)
{"".join(preview_items)}
''' # 准备详细数据用于弹窗 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) # 生成 step1 和 step3 独立详情 step1_detail_html = generate_step1_detail_html(inspiration_data) step1_detail_html_escaped = html_module.escape(step1_detail_html) step3_detail_html = generate_step3_detail_html(inspiration_data) step3_detail_html_escaped = html_module.escape(step3_detail_html) html = f'''

💡 {inspiration_name_escaped}

Top1 分数
{step1_score:.3f}
🎯 匹配要素
{step1_match_element_escaped}
{step1_match_preview} {step3_preview}
{'' if step3 else ''}
''' return html def generate_step1_detail_html(inspiration_data: Dict[str, Any]) -> str: """ 生成 step1 匹配详情的HTML Args: inspiration_data: 灵感点数据 Returns: step1 详细信息的HTML字符串 """ import html as html_module step1 = inspiration_data.get("step1", {}) inspiration_name = inspiration_data.get("inspiration_name", "未知灵感") content = f''' ''' # 获取元数据 metadata = step1.get("元数据", {}) # Step1 详细信息 if step1 and step1.get("灵感"): inspiration = step1.get("灵感", "") matches = step1.get("匹配结果列表", []) content += f''' ''' # 日志链接 if metadata.get("log_url"): content += f''' ''' return content def generate_step3_detail_html(inspiration_data: Dict[str, Any]) -> str: """ 生成 step3 生成灵感的详情HTML Args: inspiration_data: 灵感点数据 Returns: step3 详细信息的HTML字符串 """ import html as html_module inspiration_name = inspiration_data.get("inspiration_name", "未知灵感") step3 = inspiration_data.get("step3") content = f''' ''' if not step3: content += '''
暂无生成的灵感点数据
''' return content # 获取元数据 metadata = step3.get("元数据", {}) anchor_info = step3.get("锚点信息", {}) step3_inspirations = step3.get("灵感点列表", []) anchor_category = anchor_info.get("锚点分类", "") category_def = anchor_info.get("分类定义", "") category_context = anchor_info.get("分类上下文", "") content += f''' ''' # 生成的灵感点列表 if step3_inspirations: content += f''' ''' # 日志链接 if metadata.get("log_url"): content += f''' ''' return content def generate_detail_html(inspiration_data: Dict[str, Any]) -> str: """ 生成灵感点的详细信息HTML Args: inspiration_data: 灵感点数据 Returns: 详细信息的HTML字符串 """ import html as html_module step1 = inspiration_data.get("step1", {}) inspiration_name = inspiration_data.get("inspiration_name", "未知灵感") content = f''' ''' # 获取元数据,用于后面的日志链接 metadata = step1.get("元数据", {}) # Step1 详细信息 if step1 and step1.get("灵感"): inspiration = step1.get("灵感", "") matches = step1.get("匹配结果列表", []) content += f''' ''' # Step3 详细信息 step3 = inspiration_data.get("step3") if step3: anchor_info = step3.get("锚点信息", {}) step3_inspirations = step3.get("灵感点列表", []) anchor_category = anchor_info.get("锚点分类", "") category_def = anchor_info.get("分类定义", "") content += f''' ''' # 日志链接 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 showStep1Detail(element) { const step1DetailHtml = element.dataset.step1DetailHtml; const modal = document.getElementById('detailModal'); const modalBody = document.getElementById('modalBody'); modalBody.innerHTML = step1DetailHtml; modal.classList.add('active'); document.body.style.overflow = 'hidden'; } function showStep3Detail(element) { const step3DetailHtml = element.dataset.step3DetailHtml; const modal = document.getElementById('detailModal'); const modalBody = document.getElementById('modalBody'); modalBody.innerHTML = step3DetailHtml; 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 scoreA = parseFloat(a.dataset.step1Score) || 0; const scoreB = parseFloat(b.dataset.step1Score) || 0; 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 score = parseFloat(card.dataset.step1Score) || 0; totalScore += score; // 分数统计 if (score >= 0.7) excellentCount++; else if (score >= 0.5) goodCount++; else if (score >= 0.3) normalCount++; else needOptCount++; }); document.getElementById('top1ExcellentCount').textContent = excellentCount; document.getElementById('top1GoodCount').textContent = goodCount; document.getElementById('top1NormalCount').textContent = normalCount; document.getElementById('top1NeedOptCount').textContent = needOptCount; const avgScore = visibleCards.length > 0 ? (totalScore / visibleCards.length).toFixed(3) : '0.000'; document.getElementById('avgTop1Score').textContent = avgScore; } ''' 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) # 获取所有分数用于统计 def get_top1_score(d): step1 = d.get("step1", {}) matches = step1.get("匹配结果列表", []) if matches: return matches[0].get("匹配结果", {}).get("score", 0) return 0 # Top1分数统计 top1_excellent_count = sum(1 for d in inspirations_data if get_top1_score(d) >= 0.7) top1_good_count = sum(1 for d in inspirations_data if 0.5 <= get_top1_score(d) < 0.7) top1_normal_count = sum(1 for d in inspirations_data if 0.3 <= get_top1_score(d) < 0.5) top1_need_opt_count = sum(1 for d in inspirations_data if get_top1_score(d) < 0.3) # 平均分数 total_top1_score = sum(get_top1_score(d) for d in inspirations_data) avg_top1_score = total_top1_score / total_count if total_count > 0 else 0 # 按Top1分数排序 inspirations_data_sorted = sorted( inspirations_data, key=lambda x: get_top1_score(x), 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}
Top1优秀 (≥0.7)
{top1_excellent_count}
Top1良好 (0.5-0.7)
{top1_good_count}
Top1一般 (0.3-0.5)
{top1_normal_count}
Top1待优化 (<0.3)
{top1_need_opt_count}
Top1平均分
{avg_top1_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()