"""
灵感点分析结果可视化脚本
读取 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'''
{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'''
{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'''
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 += `
📋 分析信息
${metadata.current_time ? `
分析时间: ${metadata.current_time}
` : ''}
${metadata.流程 ? `
分析流程: ${metadata.流程}
` : ''}
${metadata.step1_model ? `
Step1模型: ${metadata.step1_model}
` : ''}
${metadata.step2_model ? `
Step2模型: ${metadata.step2_model}
` : ''}
`;
}
// 关键指标
content += `
📊 关键指标
Step1得分
${metrics.step1_top1_score || 0}
Step2得分
${metrics.step2_score || 0}
增量词数量
${metrics.step2_增量词数量 || 0}
匹配要素
${metrics.step1_top1_匹配要素 || '无'}
`;
// Step1 详细信息
if (step1 && step1.灵感) {
const inspiration = step1.灵感 || '';
const persona = step1.人设 || {};
const matches = step1.匹配结果 || [];
content += `
🎯 Step1: 灵感人设匹配
灵感内容:
${inspiration}
`;
// 显示匹配结果(Top3)
if (matches.length > 0) {
content += `
匹配结果 (Top ${Math.min(3, matches.length)}):
`;
matches.slice(0, 3).forEach((match, index) => {
const element = match.要素 || {};
const score = match.分数 || 0;
const reason = match.原因 || '';
const colorClass = index === 0 ? 'top1' : (index === 1 ? 'top2' : 'top3');
content += `
${element.定义 ? `
定义: ${element.定义}
` : ''}
${reason}
`;
});
content += `
`;
}
content += `
`;
}
// Step2 详细信息
if (step2 && step2.灵感) {
const step2Matches = step2.匹配结果 || [];
content += `
➕ Step2: 增量词匹配
`;
if (step2Matches.length > 0) {
content += `
增量词匹配结果:
`;
step2Matches.forEach((match, index) => {
const words = match.增量词 || [];
const score = match.分数 || 0;
const reason = match.原因 || '';
content += `
`;
});
content += `
`;
} else {
content += `
暂无增量词匹配结果
`;
}
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'''
灵感点分析可视化
'''
# 写入文件
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()