'
# 截断正文用于预览
body_preview = body_text[:80] + "..." if len(body_text) > 80 else body_text
html = f'''
{thumbnail_html}
{f'
📷 {len(images)}
' if len(images) > 1 else ''}
{html_module.escape(title)}
{html_module.escape(body_preview) if body_preview else "暂无正文"}
👤 {html_module.escape(author)}📅 {publish_time}
👍 {like_count}💬 {comment_count if comment_count else 0}⭐ {collect_count if collect_count else 0}
'''
return html
def generate_inspiration_detail_html(inspiration_point: Dict, feature_status_map: Dict[str, str] = None) -> str:
"""生成灵感点详情HTML
Args:
inspiration_point: 灵感点数据
feature_status_map: 特征名称到状态的映射 {特征名称: "相同"|"相似"|"无关"}
"""
name = inspiration_point.get("名称", "")
desc = inspiration_point.get("描述", "")
features = inspiration_point.get("特征列表", [])
if feature_status_map is None:
feature_status_map = {}
# 计算该灵感点的整体结论
feature_statuses = []
features_html_list = []
for f in features:
feature_name = f if isinstance(f, str) else f.get("特征名称", "")
weight = f.get("权重", 1.0) if isinstance(f, dict) else 1.0
# 获取该特征的状态
status = feature_status_map.get(feature_name, "无关")
feature_statuses.append(status)
if status == "相同":
status_class = "feature-same"
status_label = "相同"
elif status == "相似":
status_class = "feature-similar"
status_label = "相似"
else:
status_class = "feature-unrelated"
status_label = "无关"
features_html_list.append(
f''
f'{status_label} '
f'{html_module.escape(feature_name)} '
f'({weight})'
f''
)
features_html = "".join(features_html_list)
# 计算灵感点结论
has_same = "相同" in feature_statuses
has_similar = "相似" in feature_statuses
has_unrelated = "无关" in feature_statuses
if not has_unrelated:
# 没有无关的 -> 找到
insp_conclusion = "找到"
insp_conclusion_class = "insp-conclusion-found"
elif has_same or has_similar:
# 有相同或相似,但也有无关 -> 部分找到
insp_conclusion = "部分找到"
insp_conclusion_class = "insp-conclusion-partial"
else:
# 都是无关 -> 都找不到
insp_conclusion = "都找不到"
insp_conclusion_class = "insp-conclusion-not-found"
html = f'''
灵感点
{html_module.escape(name)}
{insp_conclusion}
描述:
{html_module.escape(desc)}
特征列表:
{features_html}
'''
return html
def load_feature_category_mapping() -> Dict:
"""加载特征名称到分类的映射"""
script_dir = Path(__file__).parent
project_root = script_dir.parent.parent
mapping_file = project_root / "data" / "data_1118" / "特征名称_分类映射.json"
try:
with open(mapping_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"警告: 无法加载特征分类映射文件: {e}")
return {}
def load_feature_source_mapping() -> Dict:
"""加载特征名称到帖子来源的映射"""
script_dir = Path(__file__).parent
project_root = script_dir.parent.parent
mapping_file = project_root / "data" / "data_1118" / "特征名称_帖子来源.json"
try:
with open(mapping_file, "r", encoding="utf-8") as f:
data = json.load(f)
# 转换为便于查询的格式: {特征名称: [来源列表]}
result = {}
for feature_type in ["灵感点", "关键点", "目的点"]:
if feature_type in data:
for item in data[feature_type]:
feature_name = item.get("特征名称")
if feature_name:
result[feature_name] = item.get("特征来源", [])
return result
except Exception as e:
print(f"警告: 无法加载特征来源映射文件: {e}")
return {}
def generate_single_match_html(match: Dict, match_idx: int, post_idx: int, insp_idx: int, feature_idx: int, category_mapping: Dict = None, source_mapping: Dict = None) -> str:
"""生成单个匹配项的HTML
Args:
match: 单个匹配数据
match_idx: 匹配项索引
post_idx: 帖子索引
insp_idx: 灵感点索引
feature_idx: 特征索引
category_mapping: 特征分类映射
source_mapping: 特征来源映射
"""
persona_name = match.get("人设特征名称", "")
feature_type = match.get("特征类型", "")
feature_categories = match.get("特征分类", [])
match_result = match.get("匹配结果", {})
similarity = match_result.get("相似度", 0.0)
explanation = match_result.get("说明", "")
# 根据相似度确定颜色和标签
if similarity >= 0.9:
color = "#10b981" # 绿色 - 相同
label = "相同"
elif similarity >= 0.8:
color = "#f59e0b" # 橙色 - 相似
label = "相似"
else:
color = "#9ca3af" # 灰色 - 无关
label = "无关"
match_id = f"post-{post_idx}-insp-{insp_idx}-feat-{feature_idx}-match-{match_idx}"
# 生成特征类型和分类标签
type_badge_html = ""
if feature_type:
type_badge_html = f'{html_module.escape(feature_type)}'
categories_badge_html = ""
if feature_categories:
categories_text = " / ".join(feature_categories)
categories_badge_html = f'{html_module.escape(categories_text)}'
# 获取该人设特征的分类信息
categories_html = ""
if category_mapping and persona_name:
found_categories = None
# 依次在灵感点、关键点、目的点中查找
for persona_type in ["灵感点", "关键点", "目的点"]:
if persona_type in category_mapping:
type_mapping = category_mapping[persona_type]
if persona_name in type_mapping:
found_categories = type_mapping[persona_name].get("所属分类", [])
break
if found_categories:
# 简洁样式:[大类/中类/小类]
categories_reversed = list(reversed(found_categories))
categories_text = "/".join(categories_reversed)
categories_html = f'[{html_module.escape(categories_text)}]'
# 获取该人设特征的历史帖子来源
historical_posts_html = ""
if source_mapping and persona_name and persona_name in source_mapping:
source_list = source_mapping[persona_name]
if source_list:
historical_cards = []
for source_item in source_list:
post_detail = source_item.get("帖子详情", {})
if post_detail:
card_html = generate_historical_post_card_html(post_detail, source_item)
historical_cards.append(card_html)
if historical_cards:
historical_posts_html = f'''
历史帖子来源
{"".join(historical_cards)}
'''
# 生成历史帖子HTML
historical_posts_html = ""
if source_mapping and persona_name and persona_name in source_mapping:
source_list = source_mapping[persona_name]
if source_list:
for source_item in source_list[:5]: # 最多5个
post_detail = source_item.get("帖子详情", {})
if post_detail:
card_html = generate_historical_post_card_html(post_detail, source_item)
historical_posts_html += card_html
# 将数据编码到data属性中
import html as html_encode
data_explanation = html_encode.escape(explanation)
data_historical = html_encode.escape(historical_posts_html)
# 生成紧凑的匹配项HTML(可点击,弹出模态框)
html = f'''
'''
return html
def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None, source_mapping: Dict = None) -> str:
"""生成包含所有帖子的单一HTML(带标签页)"""
# 生成标签页按钮
tabs_html = ""
for i, post in enumerate(posts_data):
post_detail = post.get("帖子详情", {})
title = post_detail.get("title", "无标题")
active_class = "active" if i == 0 else ""
tabs_html += f'\n'
# 生成标签页内容
contents_html = ""
for i, post in enumerate(posts_data):
active_class = "active" if i == 0 else ""
content = generate_post_content_html(post, i, category_mapping, source_mapping)
contents_html += f'''
{content}
'''
html = f'''
How解构结果可视化
How 解构结果可视化
灵感点特征匹配分析
{tabs_html}
{contents_html}
'''
return html
def main():
"""主函数"""
script_dir = Path(__file__).parent
project_root = script_dir.parent.parent
data_dir = project_root / "data" / "data_1118"
input_dir = data_dir / "当前帖子_how解构结果"
output_file = data_dir / "当前帖子_how解构结果_可视化.html"
print(f"读取 how 解构结果: {input_dir}")
# 加载特征分类映射
print(f"加载特征分类映射...")
category_mapping = load_feature_category_mapping()
print(f"已加载 {sum(len(v) for v in category_mapping.values())} 个特征分类")
# 加载特征来源映射
print(f"加载特征来源映射...")
source_mapping = load_feature_source_mapping()
print(f"已加载 {len(source_mapping)} 个特征的来源信息")
json_files = list(input_dir.glob("*_how.json"))
print(f"找到 {len(json_files)} 个文件\n")
posts_data = []
for i, file_path in enumerate(json_files, 1):
print(f"读取文件 [{i}/{len(json_files)}]: {file_path.name}")
with open(file_path, "r", encoding="utf-8") as f:
post_data = json.load(f)
posts_data.append(post_data)
print(f"\n生成合并的 HTML...")
html_content = generate_combined_html(posts_data, category_mapping, source_mapping)
print(f"保存到: {output_file}")
with open(output_file, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"\n完成! 可视化文件已保存")
print(f"请在浏览器中打开: {output_file}")
if __name__ == "__main__":
main()