#!/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'
{html_module.escape(data["选题描述"]["主题"])}
\n' html += f'
{json_filename}
\n' html += f'
生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
\n' html += '
\n' # Tab导航 html += '
\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()