#!/usr/bin/env python3
"""
脚本结果可视化工具
功能:为每个script_result_XXX.json文件生成独立的HTML可视化页面,包含三个Tab切换视图
"""
import json
import argparse
import sys
from pathlib import Path
from datetime import datetime
from typing import List, Dict, Any, Optional
import re
import html as html_module
# 保证可以从项目根目录导入 static 包
PROJECT_ROOT = Path(__file__).parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
# 导入拆分后的tab模块
from static.visualize.tab1 import generate_tab1_content
from static.visualize.tab2 import generate_tab2_content
from static.visualize.tab3 import generate_tab3_content
from static.visualize.tab5 import generate_tab5_content
class ScriptResultVisualizer:
"""脚本结果可视化器"""
def __init__(self, json_file: str = None):
"""
初始化可视化器
Args:
json_file: JSON文件路径
"""
if json_file is None:
self.json_file = None
else:
self.json_file = Path(json_file)
if not self.json_file.is_absolute():
self.json_file = Path.cwd() / json_file
def load_json_data(self, file_path: Path) -> Optional[Dict[str, Any]]:
"""
加载JSON文件
Args:
file_path: JSON文件路径
Returns:
JSON数据字典,加载失败返回None
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"加载文件失败 {file_path}: {e}")
return None
def generate_tab1_content(self, data: Dict[str, Any]) -> str:
"""生成Tab1内容:选题、灵感点、目的点、关键点"""
return generate_tab1_content(data)
def generate_tab2_content(self, data: Dict[str, Any]) -> str:
"""生成Tab2内容:段落"""
return generate_tab2_content(data)
def generate_tab3_content(self, data: Dict[str, Any]) -> str:
"""生成Tab3内容:按层次展示(实质/形式 → 具体元素/具体概念/抽象概念 → 树形展示)"""
return generate_tab3_content(data)
def generate_tab5_content(self, data: Dict[str, Any]) -> str:
"""生成tab5内容:实质点与灵感点、目的点、关键点的关系连线图"""
return generate_tab5_content(data)
def build_element_index(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
构建全局元素索引(包含实质列表和形式列表)
Args:
data: JSON数据
Returns:
元素索引字典 {element_id: element_info}
"""
element_index = {}
if '脚本理解' not in data:
return element_index
script = data['脚本理解']
# 处理实质列表
substance_list = script.get('实质列表', [])
for elem in substance_list:
elem_id = str(elem.get('id', ''))
if elem_id:
element_index[elem_id] = {
'id': elem_id,
'name': elem.get('名称', ''),
'description': elem.get('描述', ''),
'type': '实质',
'dimension': elem.get('维度', {}),
'category': elem.get('分类', {}),
'full_data': elem
}
# 处理形式列表
form_list = script.get('形式列表', [])
for elem in form_list:
elem_id = str(elem.get('id', ''))
if elem_id:
element_index[elem_id] = {
'id': elem_id,
'name': elem.get('名称', ''),
'description': elem.get('描述', ''),
'type': '形式',
'dimension': elem.get('维度', {}),
'category': elem.get('分类', {}),
'full_data': elem
}
return element_index
def highlight_element_references(self, text: str, element_index: Dict[str, Any]) -> str:
"""
在文本中标记元素引用,使其可点击查看详情
Args:
text: 待处理的文本
element_index: 全局元素索引
Returns:
处理后的HTML文本
"""
if not text or not element_index:
return html_module.escape(str(text))
result = html_module.escape(str(text))
# 按元素ID长度降序排序,避免短ID覆盖长ID (如 "1" 和 "10")
sorted_ids = sorted(element_index.keys(), key=lambda x: len(x), reverse=True)
for elem_id in sorted_ids:
elem = element_index[elem_id]
elem_name = elem.get('name', '')
# 匹配 #ID 格式 (如 "#24")
pattern_id = f'#{elem_id}\\b'
replacement_id = f'#{elem_id}'
result = re.sub(pattern_id, replacement_id, result)
# 匹配元素名称 (完整词匹配)
if elem_name:
pattern_name = f'\\b{re.escape(elem_name)}\\b'
replacement_name = f'{elem_name}'
result = re.sub(pattern_name, replacement_name, result)
return result
def format_element_id_list(self, id_list, element_index: Dict[str, Any]) -> str:
"""
将元素ID列表格式化为可点击的HTML标签
Args:
id_list: 元素ID列表或单个ID
element_index: 全局元素索引
Returns:
HTML字符串
"""
if not id_list:
return ''
html = '
\n'
# 处理单个ID或列表
ids = [id_list] if not isinstance(id_list, list) else id_list
for elem_id in ids:
elem_id_str = str(elem_id)
if elem_id_str in element_index:
elem = element_index[elem_id_str]
elem_name = elem.get('name', '')
html += f'#{elem_id_str}\n'
else:
html += f'#{elem_id_str}\n'
html += '
\n'
return html
def generate_html(self, data: Dict[str, Any], json_filename: str) -> str:
"""生成完整的HTML页面"""
# 构建全局元素索引
element_index = self.build_element_index(data)
# 开始构建HTML
html = '\n'
html += '\n'
html += '\n'
html += ' \n'
html += ' \n'
html += f' 脚本结果可视化 - {json_filename}\n'
html += ' \n'
html += '\n'
html += '\n'
html += '
\n'
# 页眉
html += '
\n'
html += '
脚本结果可视化
\n'
# 显示选题主题
if '选题描述' in data and '主题' in data['选题描述']:
html += f'
\n'
html += ' \n'
html += ' \n'
html += ' \n'
html += ' \n'
html += '
\n'
# 主内容
html += '
\n'
# Tab1内容
html += self.generate_tab1_content(data)
# Tab2内容
html += self.generate_tab2_content(data)
# Tab3内容
html += self.generate_tab3_content(data)
# tab5内容
html += self.generate_tab5_content(data)
html += '
\n'
# 页脚
html += '\n'
html += '
\n'
# JavaScript (传递元素索引)
html += '\n'
html += '\n'
html += '\n'
html += '\n'
return html
def save_all_html(self, output_dir: str | Path | None = None) -> List[str]:
"""
基于 output_demo_script.json,为其中每个视频生成一个独立的 HTML 页面。
仅支持这种结构:
{
"results": [
{
"video_data": {...},
"script_result": {...}
},
...
]
}
"""
if self.json_file is None:
print("❌ 错误: 未指定JSON文件")
return []
# 加载JSON数据
data = self.load_json_data(self.json_file)
if data is None:
return []
results = data.get("results") or []
if not isinstance(results, list) or not results:
print("⚠️ JSON 中未找到有效的 results 数组,期望为 output_demo_script.json 结构")
return []
# 确定输出目录
if output_dir is None:
# 默认输出到examples/html目录
output_dir = Path(__file__).parent / "html"
else:
output_dir = Path(output_dir)
if not output_dir.is_absolute():
output_dir = Path.cwd() / output_dir
# 创建输出目录
output_dir.mkdir(parents=True, exist_ok=True)
# 确保样式和脚本文件可用:从 static/visualize 拷贝到 输出目录/visualize
static_visualize_dir = PROJECT_ROOT / "static" / "visualize"
target_visualize_dir = output_dir / "visualize"
if static_visualize_dir.exists() and static_visualize_dir.is_dir():
import shutil
target_visualize_dir.mkdir(parents=True, exist_ok=True)
for item in static_visualize_dir.iterdir():
dst = target_visualize_dir / item.name
if item.is_file():
shutil.copy2(item, dst)
generated_paths: List[str] = []
print(f"📁 检测到 output_demo_script 格式,包含 {len(results)} 条结果")
for idx, item in enumerate(results, start=1):
script_data = item.get("script_result")
if not isinstance(script_data, dict):
print(f"⚠️ 跳过第 {idx} 条结果:缺少 script_result 字段或结构不正确")
continue
# 从 what_deconstruction_result 中获取三点解构数据并合并到 script_data
what_result = item.get("what_deconstruction_result", {})
if isinstance(what_result, dict) and "三点解构" in what_result:
deconstruction = what_result["三点解构"]
# 将三点解构数据合并到 script_data 顶层,供 tab1 使用
if "灵感点" in deconstruction:
script_data["灵感点"] = deconstruction["灵感点"]
if "目的点" in deconstruction:
script_data["目的点"] = deconstruction["目的点"]
if "关键点" in deconstruction:
script_data["关键点"] = deconstruction["关键点"]
video_data = item.get("video_data") or {}
channel_content_id = video_data.get("channel_content_id")
# 用于 HTML 内部展示的"文件名"标签
json_label = f"{self.json_file.name}#{idx}"
# 生成输出文件名(优先使用 channel_content_id,回退到序号)
if channel_content_id:
output_filename = f"script_result_{channel_content_id}.html"
else:
output_filename = f"{self.json_file.stem}_{idx}.html"
output_path = output_dir / output_filename
html_content = self.generate_html(script_data, json_label)
with open(output_path, "w", encoding="utf-8") as f:
f.write(html_content)
generated_paths.append(str(output_path))
print(f"✅ HTML文件已生成: {output_path}")
if not generated_paths:
print("⚠️ 未能从 JSON 中生成任何 HTML 文件")
return generated_paths
def main():
"""主函数"""
# 解析命令行参数
parser = argparse.ArgumentParser(
description='脚本结果可视化工具 - 基于 output_demo_script.json 为每个视频生成独立的HTML页面',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
# 在当前 examples 目录下使用默认的 output_demo_script.json 并输出到 examples/html
python visualize_script_results.py
# 指定 JSON 文件
python visualize_script_results.py examples/output_demo_script.json
# 指定 JSON 文件和输出目录
python visualize_script_results.py examples/output_demo_script.json --output-dir examples/html_script
"""
)
parser.add_argument(
'json_file',
type=str,
nargs='?',
help='JSON文件路径(默认为 examples/output_demo_script.json)'
)
parser.add_argument(
'-o', '--output-dir',
type=str,
default=None,
help='输出目录路径(默认: examples/html)'
)
args = parser.parse_args()
# 确定 JSON 文件路径
if args.json_file:
json_path = Path(args.json_file)
if not json_path.is_absolute():
json_path = Path.cwd() / json_path
else:
# 默认使用 examples/output_demo_script.json
json_path = Path(__file__).parent / "output_decode_result.json"
print("🚀 开始生成脚本结果可视化...")
print(f"📁 JSON文件: {json_path}")
print(f"📄 输出目录: {args.output_dir or (Path(__file__).parent / 'html')}")
print()
visualizer = ScriptResultVisualizer(json_file=str(json_path))
generated_files = visualizer.save_all_html(output_dir=args.output_dir)
if generated_files:
print()
print(f"🎉 完成! 共生成 {len(generated_files)} 个HTML文件")
# 提示其中一个示例文件
print(f"📄 示例: 请在浏览器中打开: {generated_files[0]}")
if __name__ == "__main__":
main()