| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841 |
- #!/usr/bin/env python3
- """
- 脚本结果可视化工具 V2
- 功能:为 output_demo_script_v2.json 中的每个视频生成独立的HTML可视化页面,专门展示"整体结构理解"的结果
- """
- import json
- import argparse
- import sys
- from pathlib import Path
- from datetime import datetime
- from typing import List, Dict, Any, Optional
- import html as html_module
- # 保证可以从项目根目录导入
- PROJECT_ROOT = Path(__file__).parent.parent
- if str(PROJECT_ROOT) not in sys.path:
- sys.path.insert(0, str(PROJECT_ROOT))
- class ScriptResultVisualizerV2:
- """脚本结果可视化器 V2 - 专门展示整体结构理解"""
- 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_overall_structure_section(self, overall_data: Dict[str, Any], section_idx: int = 0) -> str:
- """生成整体解构部分HTML"""
- html = '<div class="section overall-structure">\n'
- html += ' <h2 class="section-title collapsible" onclick="toggleCollapse(this)">整体解构 <span class="toggle-icon">▼</span></h2>\n'
- html += f' <div class="section-content collapsed" id="section-{section_idx}">\n'
- # 节点基础信息
- if "节点基础信息" in overall_data:
- html += f' <div class="subsection">\n'
- html += f' <h3 class="subsection-title collapsible" onclick="toggleCollapse(this)">节点基础信息 <span class="toggle-icon">▼</span></h3>\n'
- html += f' <div class="subsection-content collapsed" id="subsection-{section_idx}-0">\n'
- html += f' <div class="content-box">{html_module.escape(str(overall_data["节点基础信息"]))}</div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- # 整体实质×形式
- if "整体实质×形式" in overall_data:
- html += f' <div class="subsection">\n'
- html += f' <h3 class="subsection-title collapsible" onclick="toggleCollapse(this)">整体实质×形式 <span class="toggle-icon">▼</span></h3>\n'
- html += f' <div class="subsection-content collapsed" id="subsection-{section_idx}-1">\n'
- html += f' <div class="content-box">{html_module.escape(str(overall_data["整体实质×形式"]))}</div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- # 纵向逻辑流
- if "纵向逻辑流" in overall_data:
- html += f' <div class="subsection">\n'
- html += f' <h3 class="subsection-title collapsible" onclick="toggleCollapse(this)">纵向逻辑流 <span class="toggle-icon">▼</span></h3>\n'
- html += f' <div class="subsection-content collapsed" id="subsection-{section_idx}-2">\n'
- logic_flow = overall_data["纵向逻辑流"]
- if isinstance(logic_flow, list):
- html += ' <div class="logic-flow">\n'
- for idx, stage in enumerate(logic_flow):
- html += f' <div class="logic-stage collapsible-item" onclick="toggleCollapse(this)">\n'
- html += f' <div class="logic-stage-header">\n'
- if isinstance(stage, dict):
- stage_num = stage.get("阶段编号", "")
- stage_name = stage.get("阶段逻辑名称", "")
- stage_desc = stage.get("阶段逻辑描述", "")
- if stage_num:
- html += f' <div class="stage-number">阶段 {stage_num}</div>\n'
- if stage_name:
- html += f' <div class="stage-name">{html_module.escape(stage_name)}</div>\n'
- html += f' <span class="toggle-icon">▼</span>\n'
- html += f' </div>\n'
- html += f' <div class="logic-stage-content collapsed">\n'
- if stage_desc:
- html += f' <div class="stage-desc">{html_module.escape(stage_desc)}</div>\n'
- html += f' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += '</div>\n'
- return html
- def generate_paragraph_section(self, paragraphs: List[Dict[str, Any]], section_idx: int = 1) -> str:
- """生成段落解构部分HTML"""
- html = '<div class="section paragraph-structure">\n'
- html += ' <h2 class="section-title collapsible" onclick="toggleCollapse(this)">段落解构 <span class="toggle-icon">▼</span></h2>\n'
- html += f' <div class="section-content collapsed" id="section-{section_idx}">\n'
- if not isinstance(paragraphs, list):
- html += ' <p>暂无段落数据</p>\n'
- html += ' </div>\n'
- html += '</div>\n'
- return html
- for para_idx, para in enumerate(paragraphs):
- # 段落基本信息
- para_num = para.get("段落序号", "")
- time_range = para.get("时间范围", "")
- units = para.get("包含单元", [])
- full_text = para.get("段落完整文案", "")
- html += f' <div class="paragraph collapsible-item" onclick="toggleCollapse(this)">\n'
- html += f' <div class="paragraph-header">\n'
- if para_num:
- html += f' <span class="para-number">段落 {para_num}</span>\n'
- if time_range:
- html += f' <span class="time-range">{html_module.escape(time_range)}</span>\n'
- if units:
- units_str = ", ".join(str(u) for u in units) if isinstance(units, list) else str(units)
- html += f' <span class="units">包含单元: {html_module.escape(units_str)}</span>\n'
- html += f' <span class="toggle-icon">▼</span>\n'
- html += ' </div>\n'
- html += f' <div class="paragraph-content collapsed">\n'
- if full_text:
- html += f' <div class="paragraph-text">{html_module.escape(full_text)}</div>\n'
- # 具体元素实质和形式
- concrete_elements = para.get("具体元素实质和形式", [])
- if concrete_elements:
- html += f' <div class="element-group collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
- html += f' <h4 class="element-group-title">具体元素实质和形式 <span class="toggle-icon">▼</span></h4>\n'
- html += f' <div class="element-list collapsed">\n'
- for elem_idx, elem in enumerate(concrete_elements):
- html += f' <div class="element-item collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
- elem_name = elem.get("具体元素名称", "")
- if elem_name:
- html += f' <div class="element-name-header">\n'
- html += f' <span class="element-name">{html_module.escape(elem_name)}</span>\n'
- html += f' <span class="toggle-icon">▼</span>\n'
- html += f' </div>\n'
- html += f' <div class="element-forms collapsed">\n'
- for form_type in ["对应形式-文案", "对应形式-画面", "对应形式-声音"]:
- if form_type in elem:
- form_label = form_type.replace("对应形式-", "")
- html += f' <div class="form-item">\n'
- html += f' <span class="form-label">{html_module.escape(form_label)}:</span>\n'
- html += f' <span class="form-content">{html_module.escape(str(elem[form_type]))}</span>\n'
- html += f' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- # 具象概念实质和形式
- concrete_concepts = para.get("具象概念实质和形式", [])
- if concrete_concepts:
- html += f' <div class="element-group collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
- html += f' <h4 class="element-group-title">具象概念实质和形式 <span class="toggle-icon">▼</span></h4>\n'
- html += f' <div class="element-list collapsed">\n'
- for concept in concrete_concepts:
- html += f' <div class="element-item collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
- concept_name = concept.get("具象概念名称", "")
- if concept_name:
- html += f' <div class="element-name-header">\n'
- html += f' <span class="element-name">{html_module.escape(concept_name)}</span>\n'
- html += f' <span class="toggle-icon">▼</span>\n'
- html += f' </div>\n'
- html += f' <div class="element-forms collapsed">\n'
- for form_type in ["对应形式-文案", "对应形式-画面", "对应形式-声音"]:
- if form_type in concept:
- form_label = form_type.replace("对应形式-", "")
- html += f' <div class="form-item">\n'
- html += f' <span class="form-label">{html_module.escape(form_label)}:</span>\n'
- html += f' <span class="form-content">{html_module.escape(str(concept[form_type]))}</span>\n'
- html += f' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- # 抽象概念实质和形式
- abstract_concepts = para.get("抽象概念实质和形式", [])
- if abstract_concepts:
- html += f' <div class="element-group collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
- html += f' <h4 class="element-group-title">抽象概念实质和形式 <span class="toggle-icon">▼</span></h4>\n'
- html += f' <div class="element-list collapsed">\n'
- for concept in abstract_concepts:
- html += f' <div class="element-item collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
- concept_name = concept.get("抽象概念名称", "")
- if concept_name:
- html += f' <div class="element-name-header">\n'
- html += f' <span class="element-name">{html_module.escape(concept_name)}</span>\n'
- html += f' <span class="toggle-icon">▼</span>\n'
- html += f' </div>\n'
- html += f' <div class="element-forms collapsed">\n'
- for form_type in ["对应形式-文案", "对应形式-画面", "对应形式-声音"]:
- if form_type in concept:
- form_label = form_type.replace("对应形式-", "")
- html += f' <div class="form-item">\n'
- html += f' <span class="form-label">{html_module.escape(form_label)}:</span>\n'
- html += f' <span class="form-content">{html_module.escape(str(concept[form_type]))}</span>\n'
- html += f' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
- html += '</div>\n'
- return html
- def generate_html(self, understanding_data: Dict[str, Any], video_title: str, channel_content_id: str) -> str:
- """生成完整的HTML页面"""
- html = '<!DOCTYPE html>\n'
- html += '<html lang="zh-CN">\n'
- html += '<head>\n'
- html += ' <meta charset="UTF-8">\n'
- html += ' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
- html += f' <title>整体结构理解 - {html_module.escape(video_title)}</title>\n'
- html += ' <style>\n'
- html += self.generate_css()
- html += ' </style>\n'
- html += '</head>\n'
- html += '<body>\n'
- html += '<div class="container">\n'
- # 页眉
- html += '<div class="header">\n'
- html += ' <h1>整体结构理解</h1>\n'
- html += f' <div class="subtitle">{html_module.escape(video_title)}</div>\n'
- if channel_content_id:
- html += f' <div class="subtitle">ID: {html_module.escape(channel_content_id)}</div>\n'
- html += f' <div class="subtitle">生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>\n'
- html += '</div>\n'
- # 主内容
- html += '<div class="content">\n'
- # 整体解构
- if "整体解构" in understanding_data:
- html += self.generate_overall_structure_section(understanding_data["整体解构"], section_idx=0)
- # 段落解构
- if "段落解构" in understanding_data:
- html += self.generate_paragraph_section(understanding_data["段落解构"], section_idx=1)
- html += '</div>\n'
- # 页脚
- html += '<div class="footer">\n'
- html += f' <p>生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>\n'
- html += '</div>\n'
- html += '</div>\n'
- html += '<script>\n'
- html += self.generate_javascript()
- html += '</script>\n'
- html += '</body>\n'
- html += '</html>\n'
- return html
- def generate_javascript(self) -> str:
- """生成JavaScript代码"""
- return """
- function toggleCollapse(element) {
- // 阻止事件冒泡(如果是从子元素触发的)
- if (event) {
- event.stopPropagation();
- }
-
- // 查找内容区域 - 优先查找下一个兄弟元素
- let content = element.nextElementSibling;
-
- // 如果下一个兄弟元素不是内容区域,尝试在元素内部查找
- if (!content || (!content.classList.contains('collapsed') &&
- !content.classList.contains('expanded') &&
- !content.classList.contains('section-content') &&
- !content.classList.contains('subsection-content') &&
- !content.classList.contains('paragraph-content') &&
- !content.classList.contains('logic-stage-content') &&
- !content.classList.contains('element-forms') &&
- !content.classList.contains('element-list'))) {
-
- // 在元素内部查找内容区域
- content = element.querySelector('.section-content, .subsection-content, .paragraph-content, .logic-stage-content, .element-forms, .element-list');
- }
-
- // 如果还是找不到,尝试查找父元素的下一个兄弟
- if (!content && element.parentElement) {
- const siblings = Array.from(element.parentElement.children);
- const currentIndex = siblings.indexOf(element);
- if (currentIndex < siblings.length - 1) {
- content = siblings[currentIndex + 1];
- }
- }
-
- if (content) {
- // 切换展开/收起状态
- const isCollapsed = content.classList.contains('collapsed');
-
- if (isCollapsed) {
- content.classList.remove('collapsed');
- content.classList.add('expanded');
- } else {
- content.classList.remove('expanded');
- content.classList.add('collapsed');
- }
-
- // 更新图标
- const icon = element.querySelector('.toggle-icon');
- if (icon) {
- icon.textContent = isCollapsed ? '▲' : '▼';
- }
- }
- }
- """
- def generate_css(self) -> str:
- """生成CSS样式"""
- return """
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- line-height: 1.6;
- color: #333;
- background-color: #f5f5f5;
- padding: 20px;
- }
- .container {
- max-width: 1200px;
- margin: 0 auto;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
- overflow: hidden;
- }
- .header {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 30px;
- text-align: center;
- }
- .header h1 {
- font-size: 2em;
- margin-bottom: 10px;
- }
- .header .subtitle {
- font-size: 1.1em;
- opacity: 0.9;
- margin-top: 5px;
- }
- .content {
- padding: 30px;
- }
- .section {
- margin-bottom: 40px;
- }
- .section h2 {
- font-size: 1.8em;
- color: #667eea;
- margin-bottom: 20px;
- padding-bottom: 10px;
- border-bottom: 2px solid #667eea;
- }
- .section-title, .subsection-title {
- cursor: pointer;
- user-select: none;
- display: flex;
- align-items: center;
- justify-content: space-between;
- transition: background-color 0.2s;
- }
- .section-title:hover, .subsection-title:hover {
- background-color: rgba(102, 126, 234, 0.1);
- border-radius: 4px;
- padding: 5px;
- margin: -5px;
- }
- .toggle-icon {
- font-size: 0.8em;
- transition: transform 0.3s;
- margin-left: 10px;
- }
- .section-content, .subsection-content, .paragraph-content, .logic-stage-content, .element-forms, .element-list {
- max-height: 0;
- overflow: hidden;
- transition: max-height 0.3s ease-out;
- }
- .section-content.expanded, .subsection-content.expanded, .paragraph-content.expanded,
- .logic-stage-content.expanded, .element-forms.expanded, .element-list.expanded {
- max-height: 10000px;
- transition: max-height 0.5s ease-in;
- }
- .section-content.collapsed, .subsection-content.collapsed, .paragraph-content.collapsed,
- .logic-stage-content.collapsed, .element-forms.collapsed, .element-list.collapsed {
- max-height: 0;
- }
- .subsection {
- margin-bottom: 25px;
- }
- .subsection h3 {
- font-size: 1.4em;
- color: #555;
- margin-bottom: 15px;
- }
- .content-box {
- background: #f9f9f9;
- padding: 20px;
- border-radius: 6px;
- border-left: 4px solid #667eea;
- line-height: 1.8;
- white-space: pre-wrap;
- }
- .logic-flow {
- display: flex;
- flex-direction: column;
- gap: 15px;
- }
- .logic-stage {
- background: #f0f4ff;
- padding: 20px;
- border-radius: 6px;
- border-left: 4px solid #764ba2;
- }
- .stage-number {
- font-weight: bold;
- color: #764ba2;
- font-size: 1.1em;
- margin-bottom: 8px;
- }
- .stage-name {
- font-weight: bold;
- color: #333;
- font-size: 1.1em;
- margin-bottom: 10px;
- }
- .stage-desc {
- color: #666;
- line-height: 1.7;
- }
- .paragraph {
- background: #fafafa;
- border: 1px solid #e0e0e0;
- border-radius: 6px;
- padding: 20px;
- margin-bottom: 25px;
- cursor: pointer;
- transition: background-color 0.2s;
- }
- .paragraph:hover {
- background-color: #f0f0f0;
- }
- .paragraph-header {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 15px;
- margin-bottom: 15px;
- padding-bottom: 10px;
- border-bottom: 1px solid #e0e0e0;
- }
- .paragraph-header .toggle-icon {
- margin-left: auto;
- }
- .para-number {
- font-weight: bold;
- color: #667eea;
- font-size: 1.1em;
- }
- .time-range {
- color: #666;
- background: #e8e8e8;
- padding: 4px 10px;
- border-radius: 4px;
- }
- .units {
- color: #666;
- font-size: 0.9em;
- }
- .paragraph-text {
- background: white;
- padding: 15px;
- border-radius: 4px;
- margin-bottom: 20px;
- line-height: 1.8;
- border-left: 3px solid #667eea;
- }
- .element-group {
- margin-bottom: 25px;
- cursor: pointer;
- transition: background-color 0.2s;
- padding: 10px;
- border-radius: 4px;
- }
- .element-group:hover {
- background-color: rgba(0,0,0,0.02);
- }
- .element-group-title {
- font-size: 1.2em;
- color: #555;
- margin-bottom: 15px;
- padding-bottom: 8px;
- border-bottom: 1px solid #ddd;
- display: flex;
- align-items: center;
- justify-content: space-between;
- cursor: pointer;
- user-select: none;
- }
- .element-group-title:hover {
- color: #667eea;
- }
- .element-list {
- display: flex;
- flex-direction: column;
- gap: 15px;
- }
- .element-item {
- background: white;
- border: 1px solid #e0e0e0;
- border-radius: 6px;
- padding: 15px;
- transition: box-shadow 0.2s;
- cursor: pointer;
- }
- .element-item:hover {
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .element-name-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- padding-bottom: 8px;
- border-bottom: 1px solid #f0f0f0;
- }
- .element-name {
- font-weight: bold;
- color: #667eea;
- font-size: 1.1em;
- }
- .logic-stage {
- cursor: pointer;
- transition: background-color 0.2s;
- }
- .logic-stage:hover {
- background-color: #e8f0ff;
- }
- .logic-stage-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .logic-stage-header .toggle-icon {
- margin-left: auto;
- }
- .element-forms {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .form-item {
- display: flex;
- gap: 10px;
- }
- .form-label {
- font-weight: 600;
- color: #555;
- min-width: 60px;
- }
- .form-content {
- color: #666;
- flex: 1;
- line-height: 1.6;
- }
- .footer {
- background: #f9f9f9;
- padding: 20px;
- text-align: center;
- color: #666;
- border-top: 1px solid #e0e0e0;
- }
- @media (max-width: 768px) {
- .container {
- margin: 10px;
- border-radius: 4px;
- }
- .content {
- padding: 20px;
- }
- .header {
- padding: 20px;
- }
- .header h1 {
- font-size: 1.5em;
- }
- .paragraph-header {
- flex-direction: column;
- gap: 8px;
- }
- .form-item {
- flex-direction: column;
- gap: 5px;
- }
- .form-label {
- min-width: auto;
- }
- }
- """
- def save_all_html(self, output_dir: str | Path | None = None) -> List[str]:
- """
- 基于 output_demo_script_v2.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_v2.json 结构")
- return []
- # 确定输出目录
- if output_dir is None:
- # 默认输出到examples/html_v2目录
- output_dir = Path(__file__).parent / "html_v2"
- 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)
- generated_paths: List[str] = []
- print(f"📁 检测到 output_demo_script_v2 格式,包含 {len(results)} 条结果")
- for idx, item in enumerate(results, start=1):
- script_result = item.get("script_result")
- if not isinstance(script_result, dict):
- print(f"⚠️ 跳过第 {idx} 条结果:缺少 script_result 字段或结构不正确")
- continue
- understanding_data = script_result.get("整体结构理解")
- if not isinstance(understanding_data, dict):
- print(f"⚠️ 跳过第 {idx} 条结果:缺少 整体结构理解 字段或结构不正确")
- continue
- video_data = item.get("video_data") or {}
- channel_content_id = video_data.get("channel_content_id", "")
- video_title = video_data.get("title", f"视频 {idx}")
- # 生成输出文件名(优先使用 channel_content_id,回退到序号)
- if channel_content_id:
- output_filename = f"understanding_{channel_content_id}.html"
- else:
- output_filename = f"{self.json_file.stem}_understanding_{idx}.html"
- output_path = output_dir / output_filename
- html_content = self.generate_html(understanding_data, video_title, channel_content_id)
- 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='脚本结果可视化工具 V2 - 基于 output_demo_script_v2.json 为每个视频生成独立的HTML页面(展示整体结构理解)',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
- 使用示例:
- # 在当前 examples 目录下使用默认的 output_demo_script_v2.json 并输出到 examples/html_v2
- python visualize_script_results_v2.py
- # 指定 JSON 文件
- python visualize_script_results_v2.py examples/output_demo_script_v2.json
- # 指定 JSON 文件和输出目录
- python visualize_script_results_v2.py examples/output_demo_script_v2.json --output-dir examples/html_v2_custom
- """
- )
- parser.add_argument(
- 'json_file',
- type=str,
- nargs='?',
- help='JSON文件路径(默认为 examples/output_demo_script_v2.json)'
- )
- parser.add_argument(
- '-o', '--output-dir',
- type=str,
- default=None,
- help='输出目录路径(默认: examples/html_v2)'
- )
- 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_v2.json
- json_path = Path(__file__).parent / "output_demo_script_v2.json"
- print("🚀 开始生成整体结构理解可视化...")
- print(f"📁 JSON文件: {json_path}")
- print(f"📄 输出目录: {args.output_dir or (Path(__file__).parent / 'html_v2')}")
- print()
- visualizer = ScriptResultVisualizerV2(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()
|