"""
灵感点分析结果可视化脚本
读取 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'''
{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'''
{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'''
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'''
🎯 Step1: 灵感人设匹配
灵感内容:
{html_module.escape(inspiration)}
'''
# 显示匹配结果(只显示Top1)
if matches:
content += f'''
Top1匹配结果:
'''
for index, match in enumerate(matches[:1]):
input_info = match.get("输入信息", {})
match_result = match.get("匹配结果", {})
element_a = input_info.get("A", "")
context_a = input_info.get("A_Context", "")
score = match_result.get("score", 0)
score_explain = match_result.get("score说明", "")
same_parts = match_result.get("相同部分", {})
increment_parts = match_result.get("增量部分", {})
content += f'''
'''
if context_a:
content += f'
📍 所属分类: {html_module.escape(context_a).replace(chr(10), "
")}
'
if score_explain:
content += f'
💡 分数说明: {html_module.escape(score_explain)}
'
# 相同部分
if same_parts:
content += '''
'''
for key, value in same_parts.items():
content += f'''
{html_module.escape(key)}:
{html_module.escape(value)}
'''
content += '''
'''
# 增量部分
if increment_parts:
content += '''
'''
for key, value in increment_parts.items():
content += f'''
{html_module.escape(key)}:
{html_module.escape(value)}
'''
content += '''
'''
content += '''
'''
content += '''
'''
content += '''
'''
# 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 += '''
➕ Step2: 增量词匹配
'''
if increment_word.strip():
content += f'''
增量词:
{html_module.escape(increment_word)}
'''
if b_context:
content += f'''
📌 增量词来源: {html_module.escape(b_context)}
'''
content += f'''
'''
if score_explain:
content += f'''
💡 分数说明: {html_module.escape(score_explain)}
'''
# 相同部分
if same_parts:
content += '''
'''
for key, value in same_parts.items():
content += f'''
{html_module.escape(key)}:
{html_module.escape(value)}
'''
content += '''
'''
# 增量部分
if increment_parts:
content += '''
'''
for key, value in increment_parts.items():
content += f'''
{html_module.escape(key)}:
{html_module.escape(value)}
'''
content += '''
'''
content += '''
'''
else:
content += '''
暂无增量词匹配结果
'''
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'''
灵感点分析可视化
📚 人设结构
{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()