visualize_script_results_v2.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. #!/usr/bin/env python3
  2. """
  3. 脚本结果可视化工具 V2
  4. 功能:为 output_demo_script_v2.json 中的每个视频生成独立的HTML可视化页面,专门展示"整体结构理解"的结果
  5. """
  6. import json
  7. import argparse
  8. import sys
  9. from pathlib import Path
  10. from datetime import datetime
  11. from typing import List, Dict, Any, Optional
  12. import html as html_module
  13. # 保证可以从项目根目录导入
  14. PROJECT_ROOT = Path(__file__).parent.parent
  15. if str(PROJECT_ROOT) not in sys.path:
  16. sys.path.insert(0, str(PROJECT_ROOT))
  17. class ScriptResultVisualizerV2:
  18. """脚本结果可视化器 V2 - 专门展示整体结构理解"""
  19. def __init__(self, json_file: str = None):
  20. """
  21. 初始化可视化器
  22. Args:
  23. json_file: JSON文件路径
  24. """
  25. if json_file is None:
  26. self.json_file = None
  27. else:
  28. self.json_file = Path(json_file)
  29. if not self.json_file.is_absolute():
  30. self.json_file = Path.cwd() / json_file
  31. def load_json_data(self, file_path: Path) -> Optional[Dict[str, Any]]:
  32. """
  33. 加载JSON文件
  34. Args:
  35. file_path: JSON文件路径
  36. Returns:
  37. JSON数据字典,加载失败返回None
  38. """
  39. try:
  40. with open(file_path, 'r', encoding='utf-8') as f:
  41. return json.load(f)
  42. except Exception as e:
  43. print(f"加载文件失败 {file_path}: {e}")
  44. return None
  45. def generate_overall_structure_section(self, overall_data: Dict[str, Any], section_idx: int = 0) -> str:
  46. """生成整体解构部分HTML"""
  47. html = '<div class="section overall-structure">\n'
  48. html += ' <h2 class="section-title collapsible" onclick="toggleCollapse(this)">整体解构 <span class="toggle-icon">▼</span></h2>\n'
  49. html += f' <div class="section-content collapsed" id="section-{section_idx}">\n'
  50. # 节点基础信息
  51. if "节点基础信息" in overall_data:
  52. html += f' <div class="subsection">\n'
  53. html += f' <h3 class="subsection-title collapsible" onclick="toggleCollapse(this)">节点基础信息 <span class="toggle-icon">▼</span></h3>\n'
  54. html += f' <div class="subsection-content collapsed" id="subsection-{section_idx}-0">\n'
  55. html += f' <div class="content-box">{html_module.escape(str(overall_data["节点基础信息"]))}</div>\n'
  56. html += ' </div>\n'
  57. html += ' </div>\n'
  58. # 整体实质×形式
  59. if "整体实质×形式" in overall_data:
  60. html += f' <div class="subsection">\n'
  61. html += f' <h3 class="subsection-title collapsible" onclick="toggleCollapse(this)">整体实质×形式 <span class="toggle-icon">▼</span></h3>\n'
  62. html += f' <div class="subsection-content collapsed" id="subsection-{section_idx}-1">\n'
  63. html += f' <div class="content-box">{html_module.escape(str(overall_data["整体实质×形式"]))}</div>\n'
  64. html += ' </div>\n'
  65. html += ' </div>\n'
  66. # 纵向逻辑流
  67. if "纵向逻辑流" in overall_data:
  68. html += f' <div class="subsection">\n'
  69. html += f' <h3 class="subsection-title collapsible" onclick="toggleCollapse(this)">纵向逻辑流 <span class="toggle-icon">▼</span></h3>\n'
  70. html += f' <div class="subsection-content collapsed" id="subsection-{section_idx}-2">\n'
  71. logic_flow = overall_data["纵向逻辑流"]
  72. if isinstance(logic_flow, list):
  73. html += ' <div class="logic-flow">\n'
  74. for idx, stage in enumerate(logic_flow):
  75. html += f' <div class="logic-stage collapsible-item" onclick="toggleCollapse(this)">\n'
  76. html += f' <div class="logic-stage-header">\n'
  77. if isinstance(stage, dict):
  78. stage_num = stage.get("阶段编号", "")
  79. stage_name = stage.get("阶段逻辑名称", "")
  80. stage_desc = stage.get("阶段逻辑描述", "")
  81. if stage_num:
  82. html += f' <div class="stage-number">阶段 {stage_num}</div>\n'
  83. if stage_name:
  84. html += f' <div class="stage-name">{html_module.escape(stage_name)}</div>\n'
  85. html += f' <span class="toggle-icon">▼</span>\n'
  86. html += f' </div>\n'
  87. html += f' <div class="logic-stage-content collapsed">\n'
  88. if stage_desc:
  89. html += f' <div class="stage-desc">{html_module.escape(stage_desc)}</div>\n'
  90. html += f' </div>\n'
  91. html += ' </div>\n'
  92. html += ' </div>\n'
  93. html += ' </div>\n'
  94. html += ' </div>\n'
  95. html += ' </div>\n'
  96. html += '</div>\n'
  97. return html
  98. def generate_paragraph_section(self, paragraphs: List[Dict[str, Any]], section_idx: int = 1) -> str:
  99. """生成段落解构部分HTML"""
  100. html = '<div class="section paragraph-structure">\n'
  101. html += ' <h2 class="section-title collapsible" onclick="toggleCollapse(this)">段落解构 <span class="toggle-icon">▼</span></h2>\n'
  102. html += f' <div class="section-content collapsed" id="section-{section_idx}">\n'
  103. if not isinstance(paragraphs, list):
  104. html += ' <p>暂无段落数据</p>\n'
  105. html += ' </div>\n'
  106. html += '</div>\n'
  107. return html
  108. for para_idx, para in enumerate(paragraphs):
  109. # 段落基本信息
  110. para_num = para.get("段落序号", "")
  111. time_range = para.get("时间范围", "")
  112. units = para.get("包含单元", [])
  113. full_text = para.get("段落完整文案", "")
  114. html += f' <div class="paragraph collapsible-item" onclick="toggleCollapse(this)">\n'
  115. html += f' <div class="paragraph-header">\n'
  116. if para_num:
  117. html += f' <span class="para-number">段落 {para_num}</span>\n'
  118. if time_range:
  119. html += f' <span class="time-range">{html_module.escape(time_range)}</span>\n'
  120. if units:
  121. units_str = ", ".join(str(u) for u in units) if isinstance(units, list) else str(units)
  122. html += f' <span class="units">包含单元: {html_module.escape(units_str)}</span>\n'
  123. html += f' <span class="toggle-icon">▼</span>\n'
  124. html += ' </div>\n'
  125. html += f' <div class="paragraph-content collapsed">\n'
  126. if full_text:
  127. html += f' <div class="paragraph-text">{html_module.escape(full_text)}</div>\n'
  128. # 具体元素实质和形式
  129. concrete_elements = para.get("具体元素实质和形式", [])
  130. if concrete_elements:
  131. html += f' <div class="element-group collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
  132. html += f' <h4 class="element-group-title">具体元素实质和形式 <span class="toggle-icon">▼</span></h4>\n'
  133. html += f' <div class="element-list collapsed">\n'
  134. for elem_idx, elem in enumerate(concrete_elements):
  135. html += f' <div class="element-item collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
  136. elem_name = elem.get("具体元素名称", "")
  137. if elem_name:
  138. html += f' <div class="element-name-header">\n'
  139. html += f' <span class="element-name">{html_module.escape(elem_name)}</span>\n'
  140. html += f' <span class="toggle-icon">▼</span>\n'
  141. html += f' </div>\n'
  142. html += f' <div class="element-forms collapsed">\n'
  143. for form_type in ["对应形式-文案", "对应形式-画面", "对应形式-声音"]:
  144. if form_type in elem:
  145. form_label = form_type.replace("对应形式-", "")
  146. html += f' <div class="form-item">\n'
  147. html += f' <span class="form-label">{html_module.escape(form_label)}:</span>\n'
  148. html += f' <span class="form-content">{html_module.escape(str(elem[form_type]))}</span>\n'
  149. html += f' </div>\n'
  150. html += ' </div>\n'
  151. html += ' </div>\n'
  152. html += ' </div>\n'
  153. html += ' </div>\n'
  154. # 具象概念实质和形式
  155. concrete_concepts = para.get("具象概念实质和形式", [])
  156. if concrete_concepts:
  157. html += f' <div class="element-group collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
  158. html += f' <h4 class="element-group-title">具象概念实质和形式 <span class="toggle-icon">▼</span></h4>\n'
  159. html += f' <div class="element-list collapsed">\n'
  160. for concept in concrete_concepts:
  161. html += f' <div class="element-item collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
  162. concept_name = concept.get("具象概念名称", "")
  163. if concept_name:
  164. html += f' <div class="element-name-header">\n'
  165. html += f' <span class="element-name">{html_module.escape(concept_name)}</span>\n'
  166. html += f' <span class="toggle-icon">▼</span>\n'
  167. html += f' </div>\n'
  168. html += f' <div class="element-forms collapsed">\n'
  169. for form_type in ["对应形式-文案", "对应形式-画面", "对应形式-声音"]:
  170. if form_type in concept:
  171. form_label = form_type.replace("对应形式-", "")
  172. html += f' <div class="form-item">\n'
  173. html += f' <span class="form-label">{html_module.escape(form_label)}:</span>\n'
  174. html += f' <span class="form-content">{html_module.escape(str(concept[form_type]))}</span>\n'
  175. html += f' </div>\n'
  176. html += ' </div>\n'
  177. html += ' </div>\n'
  178. html += ' </div>\n'
  179. html += ' </div>\n'
  180. # 抽象概念实质和形式
  181. abstract_concepts = para.get("抽象概念实质和形式", [])
  182. if abstract_concepts:
  183. html += f' <div class="element-group collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
  184. html += f' <h4 class="element-group-title">抽象概念实质和形式 <span class="toggle-icon">▼</span></h4>\n'
  185. html += f' <div class="element-list collapsed">\n'
  186. for concept in abstract_concepts:
  187. html += f' <div class="element-item collapsible-item" onclick="event.stopPropagation(); toggleCollapse(this);">\n'
  188. concept_name = concept.get("抽象概念名称", "")
  189. if concept_name:
  190. html += f' <div class="element-name-header">\n'
  191. html += f' <span class="element-name">{html_module.escape(concept_name)}</span>\n'
  192. html += f' <span class="toggle-icon">▼</span>\n'
  193. html += f' </div>\n'
  194. html += f' <div class="element-forms collapsed">\n'
  195. for form_type in ["对应形式-文案", "对应形式-画面", "对应形式-声音"]:
  196. if form_type in concept:
  197. form_label = form_type.replace("对应形式-", "")
  198. html += f' <div class="form-item">\n'
  199. html += f' <span class="form-label">{html_module.escape(form_label)}:</span>\n'
  200. html += f' <span class="form-content">{html_module.escape(str(concept[form_type]))}</span>\n'
  201. html += f' </div>\n'
  202. html += ' </div>\n'
  203. html += ' </div>\n'
  204. html += ' </div>\n'
  205. html += ' </div>\n'
  206. html += ' </div>\n'
  207. html += ' </div>\n'
  208. html += ' </div>\n'
  209. html += '</div>\n'
  210. return html
  211. def generate_html(self, understanding_data: Dict[str, Any], video_title: str, channel_content_id: str) -> str:
  212. """生成完整的HTML页面"""
  213. html = '<!DOCTYPE html>\n'
  214. html += '<html lang="zh-CN">\n'
  215. html += '<head>\n'
  216. html += ' <meta charset="UTF-8">\n'
  217. html += ' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
  218. html += f' <title>整体结构理解 - {html_module.escape(video_title)}</title>\n'
  219. html += ' <style>\n'
  220. html += self.generate_css()
  221. html += ' </style>\n'
  222. html += '</head>\n'
  223. html += '<body>\n'
  224. html += '<div class="container">\n'
  225. # 页眉
  226. html += '<div class="header">\n'
  227. html += ' <h1>整体结构理解</h1>\n'
  228. html += f' <div class="subtitle">{html_module.escape(video_title)}</div>\n'
  229. if channel_content_id:
  230. html += f' <div class="subtitle">ID: {html_module.escape(channel_content_id)}</div>\n'
  231. html += f' <div class="subtitle">生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>\n'
  232. html += '</div>\n'
  233. # 主内容
  234. html += '<div class="content">\n'
  235. # 整体解构
  236. if "整体解构" in understanding_data:
  237. html += self.generate_overall_structure_section(understanding_data["整体解构"], section_idx=0)
  238. # 段落解构
  239. if "段落解构" in understanding_data:
  240. html += self.generate_paragraph_section(understanding_data["段落解构"], section_idx=1)
  241. html += '</div>\n'
  242. # 页脚
  243. html += '<div class="footer">\n'
  244. html += f' <p>生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>\n'
  245. html += '</div>\n'
  246. html += '</div>\n'
  247. html += '<script>\n'
  248. html += self.generate_javascript()
  249. html += '</script>\n'
  250. html += '</body>\n'
  251. html += '</html>\n'
  252. return html
  253. def generate_javascript(self) -> str:
  254. """生成JavaScript代码"""
  255. return """
  256. function toggleCollapse(element) {
  257. // 阻止事件冒泡(如果是从子元素触发的)
  258. if (event) {
  259. event.stopPropagation();
  260. }
  261. // 查找内容区域 - 优先查找下一个兄弟元素
  262. let content = element.nextElementSibling;
  263. // 如果下一个兄弟元素不是内容区域,尝试在元素内部查找
  264. if (!content || (!content.classList.contains('collapsed') &&
  265. !content.classList.contains('expanded') &&
  266. !content.classList.contains('section-content') &&
  267. !content.classList.contains('subsection-content') &&
  268. !content.classList.contains('paragraph-content') &&
  269. !content.classList.contains('logic-stage-content') &&
  270. !content.classList.contains('element-forms') &&
  271. !content.classList.contains('element-list'))) {
  272. // 在元素内部查找内容区域
  273. content = element.querySelector('.section-content, .subsection-content, .paragraph-content, .logic-stage-content, .element-forms, .element-list');
  274. }
  275. // 如果还是找不到,尝试查找父元素的下一个兄弟
  276. if (!content && element.parentElement) {
  277. const siblings = Array.from(element.parentElement.children);
  278. const currentIndex = siblings.indexOf(element);
  279. if (currentIndex < siblings.length - 1) {
  280. content = siblings[currentIndex + 1];
  281. }
  282. }
  283. if (content) {
  284. // 切换展开/收起状态
  285. const isCollapsed = content.classList.contains('collapsed');
  286. if (isCollapsed) {
  287. content.classList.remove('collapsed');
  288. content.classList.add('expanded');
  289. } else {
  290. content.classList.remove('expanded');
  291. content.classList.add('collapsed');
  292. }
  293. // 更新图标
  294. const icon = element.querySelector('.toggle-icon');
  295. if (icon) {
  296. icon.textContent = isCollapsed ? '▲' : '▼';
  297. }
  298. }
  299. }
  300. """
  301. def generate_css(self) -> str:
  302. """生成CSS样式"""
  303. return """
  304. * {
  305. margin: 0;
  306. padding: 0;
  307. box-sizing: border-box;
  308. }
  309. body {
  310. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  311. line-height: 1.6;
  312. color: #333;
  313. background-color: #f5f5f5;
  314. padding: 20px;
  315. }
  316. .container {
  317. max-width: 1200px;
  318. margin: 0 auto;
  319. background: white;
  320. border-radius: 8px;
  321. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  322. overflow: hidden;
  323. }
  324. .header {
  325. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  326. color: white;
  327. padding: 30px;
  328. text-align: center;
  329. }
  330. .header h1 {
  331. font-size: 2em;
  332. margin-bottom: 10px;
  333. }
  334. .header .subtitle {
  335. font-size: 1.1em;
  336. opacity: 0.9;
  337. margin-top: 5px;
  338. }
  339. .content {
  340. padding: 30px;
  341. }
  342. .section {
  343. margin-bottom: 40px;
  344. }
  345. .section h2 {
  346. font-size: 1.8em;
  347. color: #667eea;
  348. margin-bottom: 20px;
  349. padding-bottom: 10px;
  350. border-bottom: 2px solid #667eea;
  351. }
  352. .section-title, .subsection-title {
  353. cursor: pointer;
  354. user-select: none;
  355. display: flex;
  356. align-items: center;
  357. justify-content: space-between;
  358. transition: background-color 0.2s;
  359. }
  360. .section-title:hover, .subsection-title:hover {
  361. background-color: rgba(102, 126, 234, 0.1);
  362. border-radius: 4px;
  363. padding: 5px;
  364. margin: -5px;
  365. }
  366. .toggle-icon {
  367. font-size: 0.8em;
  368. transition: transform 0.3s;
  369. margin-left: 10px;
  370. }
  371. .section-content, .subsection-content, .paragraph-content, .logic-stage-content, .element-forms, .element-list {
  372. max-height: 0;
  373. overflow: hidden;
  374. transition: max-height 0.3s ease-out;
  375. }
  376. .section-content.expanded, .subsection-content.expanded, .paragraph-content.expanded,
  377. .logic-stage-content.expanded, .element-forms.expanded, .element-list.expanded {
  378. max-height: 10000px;
  379. transition: max-height 0.5s ease-in;
  380. }
  381. .section-content.collapsed, .subsection-content.collapsed, .paragraph-content.collapsed,
  382. .logic-stage-content.collapsed, .element-forms.collapsed, .element-list.collapsed {
  383. max-height: 0;
  384. }
  385. .subsection {
  386. margin-bottom: 25px;
  387. }
  388. .subsection h3 {
  389. font-size: 1.4em;
  390. color: #555;
  391. margin-bottom: 15px;
  392. }
  393. .content-box {
  394. background: #f9f9f9;
  395. padding: 20px;
  396. border-radius: 6px;
  397. border-left: 4px solid #667eea;
  398. line-height: 1.8;
  399. white-space: pre-wrap;
  400. }
  401. .logic-flow {
  402. display: flex;
  403. flex-direction: column;
  404. gap: 15px;
  405. }
  406. .logic-stage {
  407. background: #f0f4ff;
  408. padding: 20px;
  409. border-radius: 6px;
  410. border-left: 4px solid #764ba2;
  411. }
  412. .stage-number {
  413. font-weight: bold;
  414. color: #764ba2;
  415. font-size: 1.1em;
  416. margin-bottom: 8px;
  417. }
  418. .stage-name {
  419. font-weight: bold;
  420. color: #333;
  421. font-size: 1.1em;
  422. margin-bottom: 10px;
  423. }
  424. .stage-desc {
  425. color: #666;
  426. line-height: 1.7;
  427. }
  428. .paragraph {
  429. background: #fafafa;
  430. border: 1px solid #e0e0e0;
  431. border-radius: 6px;
  432. padding: 20px;
  433. margin-bottom: 25px;
  434. cursor: pointer;
  435. transition: background-color 0.2s;
  436. }
  437. .paragraph:hover {
  438. background-color: #f0f0f0;
  439. }
  440. .paragraph-header {
  441. display: flex;
  442. flex-wrap: wrap;
  443. align-items: center;
  444. gap: 15px;
  445. margin-bottom: 15px;
  446. padding-bottom: 10px;
  447. border-bottom: 1px solid #e0e0e0;
  448. }
  449. .paragraph-header .toggle-icon {
  450. margin-left: auto;
  451. }
  452. .para-number {
  453. font-weight: bold;
  454. color: #667eea;
  455. font-size: 1.1em;
  456. }
  457. .time-range {
  458. color: #666;
  459. background: #e8e8e8;
  460. padding: 4px 10px;
  461. border-radius: 4px;
  462. }
  463. .units {
  464. color: #666;
  465. font-size: 0.9em;
  466. }
  467. .paragraph-text {
  468. background: white;
  469. padding: 15px;
  470. border-radius: 4px;
  471. margin-bottom: 20px;
  472. line-height: 1.8;
  473. border-left: 3px solid #667eea;
  474. }
  475. .element-group {
  476. margin-bottom: 25px;
  477. cursor: pointer;
  478. transition: background-color 0.2s;
  479. padding: 10px;
  480. border-radius: 4px;
  481. }
  482. .element-group:hover {
  483. background-color: rgba(0,0,0,0.02);
  484. }
  485. .element-group-title {
  486. font-size: 1.2em;
  487. color: #555;
  488. margin-bottom: 15px;
  489. padding-bottom: 8px;
  490. border-bottom: 1px solid #ddd;
  491. display: flex;
  492. align-items: center;
  493. justify-content: space-between;
  494. cursor: pointer;
  495. user-select: none;
  496. }
  497. .element-group-title:hover {
  498. color: #667eea;
  499. }
  500. .element-list {
  501. display: flex;
  502. flex-direction: column;
  503. gap: 15px;
  504. }
  505. .element-item {
  506. background: white;
  507. border: 1px solid #e0e0e0;
  508. border-radius: 6px;
  509. padding: 15px;
  510. transition: box-shadow 0.2s;
  511. cursor: pointer;
  512. }
  513. .element-item:hover {
  514. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  515. }
  516. .element-name-header {
  517. display: flex;
  518. align-items: center;
  519. justify-content: space-between;
  520. margin-bottom: 12px;
  521. padding-bottom: 8px;
  522. border-bottom: 1px solid #f0f0f0;
  523. }
  524. .element-name {
  525. font-weight: bold;
  526. color: #667eea;
  527. font-size: 1.1em;
  528. }
  529. .logic-stage {
  530. cursor: pointer;
  531. transition: background-color 0.2s;
  532. }
  533. .logic-stage:hover {
  534. background-color: #e8f0ff;
  535. }
  536. .logic-stage-header {
  537. display: flex;
  538. align-items: center;
  539. justify-content: space-between;
  540. }
  541. .logic-stage-header .toggle-icon {
  542. margin-left: auto;
  543. }
  544. .element-forms {
  545. display: flex;
  546. flex-direction: column;
  547. gap: 10px;
  548. }
  549. .form-item {
  550. display: flex;
  551. gap: 10px;
  552. }
  553. .form-label {
  554. font-weight: 600;
  555. color: #555;
  556. min-width: 60px;
  557. }
  558. .form-content {
  559. color: #666;
  560. flex: 1;
  561. line-height: 1.6;
  562. }
  563. .footer {
  564. background: #f9f9f9;
  565. padding: 20px;
  566. text-align: center;
  567. color: #666;
  568. border-top: 1px solid #e0e0e0;
  569. }
  570. @media (max-width: 768px) {
  571. .container {
  572. margin: 10px;
  573. border-radius: 4px;
  574. }
  575. .content {
  576. padding: 20px;
  577. }
  578. .header {
  579. padding: 20px;
  580. }
  581. .header h1 {
  582. font-size: 1.5em;
  583. }
  584. .paragraph-header {
  585. flex-direction: column;
  586. gap: 8px;
  587. }
  588. .form-item {
  589. flex-direction: column;
  590. gap: 5px;
  591. }
  592. .form-label {
  593. min-width: auto;
  594. }
  595. }
  596. """
  597. def save_all_html(self, output_dir: str | Path | None = None) -> List[str]:
  598. """
  599. 基于 output_demo_script_v2.json,为其中每个视频生成一个独立的 HTML 页面。
  600. 仅支持这种结构:
  601. {
  602. "results": [
  603. {
  604. "video_data": {...},
  605. "script_result": {
  606. "整体结构理解": {...}
  607. }
  608. },
  609. ...
  610. ]
  611. }
  612. """
  613. if self.json_file is None:
  614. print("❌ 错误: 未指定JSON文件")
  615. return []
  616. # 加载JSON数据
  617. data = self.load_json_data(self.json_file)
  618. if data is None:
  619. return []
  620. results = data.get("results") or []
  621. if not isinstance(results, list) or not results:
  622. print("⚠️ JSON 中未找到有效的 results 数组,期望为 output_demo_script_v2.json 结构")
  623. return []
  624. # 确定输出目录
  625. if output_dir is None:
  626. # 默认输出到examples/html_v2目录
  627. output_dir = Path(__file__).parent / "html_v2"
  628. else:
  629. output_dir = Path(output_dir)
  630. if not output_dir.is_absolute():
  631. output_dir = Path.cwd() / output_dir
  632. # 创建输出目录
  633. output_dir.mkdir(parents=True, exist_ok=True)
  634. generated_paths: List[str] = []
  635. print(f"📁 检测到 output_demo_script_v2 格式,包含 {len(results)} 条结果")
  636. for idx, item in enumerate(results, start=1):
  637. script_result = item.get("script_result")
  638. if not isinstance(script_result, dict):
  639. print(f"⚠️ 跳过第 {idx} 条结果:缺少 script_result 字段或结构不正确")
  640. continue
  641. understanding_data = script_result.get("整体结构理解")
  642. if not isinstance(understanding_data, dict):
  643. print(f"⚠️ 跳过第 {idx} 条结果:缺少 整体结构理解 字段或结构不正确")
  644. continue
  645. video_data = item.get("video_data") or {}
  646. channel_content_id = video_data.get("channel_content_id", "")
  647. video_title = video_data.get("title", f"视频 {idx}")
  648. # 生成输出文件名(优先使用 channel_content_id,回退到序号)
  649. if channel_content_id:
  650. output_filename = f"understanding_{channel_content_id}.html"
  651. else:
  652. output_filename = f"{self.json_file.stem}_understanding_{idx}.html"
  653. output_path = output_dir / output_filename
  654. html_content = self.generate_html(understanding_data, video_title, channel_content_id)
  655. with open(output_path, "w", encoding="utf-8") as f:
  656. f.write(html_content)
  657. generated_paths.append(str(output_path))
  658. print(f"✅ HTML文件已生成: {output_path}")
  659. if not generated_paths:
  660. print("⚠️ 未能从 JSON 中生成任何 HTML 文件")
  661. return generated_paths
  662. def main():
  663. """主函数"""
  664. # 解析命令行参数
  665. parser = argparse.ArgumentParser(
  666. description='脚本结果可视化工具 V2 - 基于 output_demo_script_v2.json 为每个视频生成独立的HTML页面(展示整体结构理解)',
  667. formatter_class=argparse.RawDescriptionHelpFormatter,
  668. epilog="""
  669. 使用示例:
  670. # 在当前 examples 目录下使用默认的 output_demo_script_v2.json 并输出到 examples/html_v2
  671. python visualize_script_results_v2.py
  672. # 指定 JSON 文件
  673. python visualize_script_results_v2.py examples/output_demo_script_v2.json
  674. # 指定 JSON 文件和输出目录
  675. python visualize_script_results_v2.py examples/output_demo_script_v2.json --output-dir examples/html_v2_custom
  676. """
  677. )
  678. parser.add_argument(
  679. 'json_file',
  680. type=str,
  681. nargs='?',
  682. help='JSON文件路径(默认为 examples/output_demo_script_v2.json)'
  683. )
  684. parser.add_argument(
  685. '-o', '--output-dir',
  686. type=str,
  687. default=None,
  688. help='输出目录路径(默认: examples/html_v2)'
  689. )
  690. args = parser.parse_args()
  691. # 确定 JSON 文件路径
  692. if args.json_file:
  693. json_path = Path(args.json_file)
  694. if not json_path.is_absolute():
  695. json_path = Path.cwd() / json_path
  696. else:
  697. # 默认使用 examples/output_demo_script_v2.json
  698. json_path = Path(__file__).parent / "output_demo_script_v2.json"
  699. print("🚀 开始生成整体结构理解可视化...")
  700. print(f"📁 JSON文件: {json_path}")
  701. print(f"📄 输出目录: {args.output_dir or (Path(__file__).parent / 'html_v2')}")
  702. print()
  703. visualizer = ScriptResultVisualizerV2(json_file=str(json_path))
  704. generated_files = visualizer.save_all_html(output_dir=args.output_dir)
  705. if generated_files:
  706. print()
  707. print(f"🎉 完成! 共生成 {len(generated_files)} 个HTML文件")
  708. # 提示其中一个示例文件
  709. print(f"📄 示例: 请在浏览器中打开: {generated_files[0]}")
  710. if __name__ == "__main__":
  711. main()