visualize_stage6_results.py 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Stage6评估结果可视化工具
  5. 整合两层评估结果的交互式HTML页面
  6. """
  7. import json
  8. import os
  9. from datetime import datetime
  10. from typing import List, Dict, Any
  11. def load_data(json_path: str) -> List[Dict[str, Any]]:
  12. """加载JSON数据"""
  13. with open(json_path, 'r', encoding='utf-8') as f:
  14. return json.load(f)
  15. def calculate_statistics(data: List[Dict[str, Any]]) -> Dict[str, Any]:
  16. """计算统计数据(包括评估结果)"""
  17. total_features = len(data)
  18. total_search_words = 0
  19. total_notes = 0
  20. video_count = 0
  21. normal_count = 0
  22. # 评估统计
  23. total_evaluated_notes = 0
  24. total_filtered = 0
  25. match_complete = 0 # 8-10分
  26. match_similar = 0 # 6-7分
  27. match_weak = 0 # 5-6分
  28. match_none = 0 # ≤4分
  29. for feature in data:
  30. grouped_results = feature.get('组合评估结果_分组', [])
  31. for group in grouped_results:
  32. search_items = group.get('top10_searches', [])
  33. total_search_words += len(search_items)
  34. for search_item in search_items:
  35. search_result = search_item.get('search_result', {})
  36. notes = search_result.get('data', {}).get('data', [])
  37. total_notes += len(notes)
  38. # 统计视频/图文类型
  39. for note in notes:
  40. note_type = note.get('note_card', {}).get('type', '')
  41. if note_type == 'video':
  42. video_count += 1
  43. else:
  44. normal_count += 1
  45. # 统计评估结果
  46. evaluation = search_item.get('evaluation_with_filter')
  47. if evaluation:
  48. total_evaluated_notes += evaluation.get('total_notes', 0)
  49. total_filtered += evaluation.get('filtered_count', 0)
  50. stats = evaluation.get('statistics', {})
  51. match_complete += stats.get('完全匹配(8-10)', 0)
  52. match_similar += stats.get('相似匹配(6-7)', 0)
  53. match_weak += stats.get('弱相似(5-6)', 0)
  54. match_none += stats.get('无匹配(≤4)', 0)
  55. # 计算百分比
  56. total_remaining = total_evaluated_notes - total_filtered if total_evaluated_notes > 0 else 0
  57. return {
  58. 'total_features': total_features,
  59. 'total_search_words': total_search_words,
  60. 'total_notes': total_notes,
  61. 'video_count': video_count,
  62. 'normal_count': normal_count,
  63. 'video_percentage': round(video_count / total_notes * 100, 1) if total_notes > 0 else 0,
  64. 'normal_percentage': round(normal_count / total_notes * 100, 1) if total_notes > 0 else 0,
  65. # 评估统计
  66. 'total_evaluated': total_evaluated_notes,
  67. 'total_filtered': total_filtered,
  68. 'total_remaining': total_remaining,
  69. 'filter_rate': round(total_filtered / total_evaluated_notes * 100, 1) if total_evaluated_notes > 0 else 0,
  70. 'match_complete': match_complete,
  71. 'match_similar': match_similar,
  72. 'match_weak': match_weak,
  73. 'match_none': match_none,
  74. 'complete_rate': round(match_complete / total_remaining * 100, 1) if total_remaining > 0 else 0,
  75. 'similar_rate': round(match_similar / total_remaining * 100, 1) if total_remaining > 0 else 0,
  76. }
  77. def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any], output_path: str):
  78. """生成HTML可视化页面"""
  79. # 准备数据JSON(用于JavaScript)
  80. data_json = json.dumps(data, ensure_ascii=False, indent=2)
  81. html_content = f'''<!DOCTYPE html>
  82. <html lang="zh-CN">
  83. <head>
  84. <meta charset="UTF-8">
  85. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  86. <title>Stage6 评估结果可视化</title>
  87. <style>
  88. * {{
  89. margin: 0;
  90. padding: 0;
  91. box-sizing: border-box;
  92. }}
  93. body {{
  94. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  95. background: #f5f7fa;
  96. color: #333;
  97. overflow-x: hidden;
  98. }}
  99. /* 顶部统计面板 */
  100. .stats-panel {{
  101. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  102. color: white;
  103. padding: 20px;
  104. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  105. }}
  106. .stats-container {{
  107. max-width: 1400px;
  108. margin: 0 auto;
  109. }}
  110. .stats-row {{
  111. display: flex;
  112. justify-content: space-around;
  113. align-items: center;
  114. flex-wrap: wrap;
  115. gap: 15px;
  116. margin-bottom: 15px;
  117. }}
  118. .stats-row:last-child {{
  119. margin-bottom: 0;
  120. padding-top: 15px;
  121. border-top: 1px solid rgba(255,255,255,0.2);
  122. }}
  123. .stat-item {{
  124. text-align: center;
  125. }}
  126. .stat-value {{
  127. font-size: 28px;
  128. font-weight: bold;
  129. margin-bottom: 5px;
  130. }}
  131. .stat-label {{
  132. font-size: 12px;
  133. opacity: 0.9;
  134. }}
  135. .stat-item.small .stat-value {{
  136. font-size: 22px;
  137. }}
  138. /* 过滤控制面板 */
  139. .filter-panel {{
  140. background: white;
  141. max-width: 1400px;
  142. margin: 20px auto;
  143. padding: 15px 20px;
  144. border-radius: 8px;
  145. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  146. display: flex;
  147. align-items: center;
  148. gap: 20px;
  149. flex-wrap: wrap;
  150. }}
  151. .filter-label {{
  152. font-weight: 600;
  153. color: #374151;
  154. }}
  155. .filter-buttons {{
  156. display: flex;
  157. gap: 10px;
  158. flex-wrap: wrap;
  159. }}
  160. .filter-btn {{
  161. padding: 6px 12px;
  162. border: 2px solid #e5e7eb;
  163. background: white;
  164. border-radius: 6px;
  165. cursor: pointer;
  166. font-size: 13px;
  167. font-weight: 500;
  168. transition: all 0.2s;
  169. }}
  170. .filter-btn:hover {{
  171. border-color: #667eea;
  172. background: #f9fafb;
  173. }}
  174. .filter-btn.active {{
  175. border-color: #667eea;
  176. background: #667eea;
  177. color: white;
  178. }}
  179. .filter-btn.complete {{
  180. border-color: #10b981;
  181. }}
  182. .filter-btn.complete.active {{
  183. background: #10b981;
  184. border-color: #10b981;
  185. }}
  186. .filter-btn.similar {{
  187. border-color: #f59e0b;
  188. }}
  189. .filter-btn.similar.active {{
  190. background: #f59e0b;
  191. border-color: #f59e0b;
  192. }}
  193. .filter-btn.weak {{
  194. border-color: #f97316;
  195. }}
  196. .filter-btn.weak.active {{
  197. background: #f97316;
  198. border-color: #f97316;
  199. }}
  200. .filter-btn.none {{
  201. border-color: #ef4444;
  202. }}
  203. .filter-btn.none.active {{
  204. background: #ef4444;
  205. border-color: #ef4444;
  206. }}
  207. .filter-btn.filtered {{
  208. border-color: #6b7280;
  209. }}
  210. .filter-btn.filtered.active {{
  211. background: #6b7280;
  212. border-color: #6b7280;
  213. }}
  214. /* 主容器 */
  215. .main-container {{
  216. display: flex;
  217. max-width: 1400px;
  218. margin: 0 auto 20px;
  219. gap: 20px;
  220. padding: 0 20px;
  221. height: calc(100vh - 260px);
  222. }}
  223. /* 左侧导航 */
  224. .left-sidebar {{
  225. width: 30%;
  226. background: white;
  227. border-radius: 8px;
  228. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  229. overflow-y: auto;
  230. position: sticky;
  231. top: 20px;
  232. height: fit-content;
  233. max-height: calc(100vh - 280px);
  234. }}
  235. .feature-group {{
  236. border-bottom: 1px solid #e5e7eb;
  237. }}
  238. .feature-header {{
  239. padding: 15px 20px;
  240. background: #f9fafb;
  241. cursor: pointer;
  242. user-select: none;
  243. transition: background 0.2s;
  244. }}
  245. .feature-header:hover {{
  246. background: #f3f4f6;
  247. }}
  248. .feature-header.active {{
  249. background: #667eea;
  250. color: white;
  251. }}
  252. .feature-title {{
  253. font-size: 16px;
  254. font-weight: 600;
  255. margin-bottom: 5px;
  256. }}
  257. .feature-meta {{
  258. font-size: 12px;
  259. color: #6b7280;
  260. }}
  261. .feature-header.active .feature-meta {{
  262. color: rgba(255,255,255,0.8);
  263. }}
  264. .search-words-list {{
  265. display: none;
  266. padding: 0;
  267. }}
  268. .search-words-list.expanded {{
  269. display: block;
  270. }}
  271. /* Base word分组层 */
  272. .base-word-group {{
  273. border-bottom: 1px solid #f3f4f6;
  274. }}
  275. .base-word-header {{
  276. padding: 12px 20px 12px 30px;
  277. background: #fafbfc;
  278. cursor: pointer;
  279. user-select: none;
  280. transition: all 0.2s;
  281. border-left: 3px solid transparent;
  282. }}
  283. .base-word-header:hover {{
  284. background: #f3f4f6;
  285. border-left-color: #a78bfa;
  286. }}
  287. .base-word-header.active {{
  288. background: #f3f4f6;
  289. border-left-color: #7c3aed;
  290. }}
  291. .base-word-title {{
  292. font-size: 15px;
  293. font-weight: 600;
  294. color: #7c3aed;
  295. margin-bottom: 4px;
  296. }}
  297. .base-word-meta {{
  298. font-size: 11px;
  299. color: #6b7280;
  300. }}
  301. .base-word-desc {{
  302. padding: 8px 20px 8px 30px;
  303. background: #fefce8;
  304. font-size: 12px;
  305. color: #854d0e;
  306. line-height: 1.5;
  307. border-left: 3px solid #fbbf24;
  308. display: none;
  309. }}
  310. .base-word-desc.expanded {{
  311. display: block;
  312. }}
  313. .search-words-sublist {{
  314. display: none;
  315. }}
  316. .search-words-sublist.expanded {{
  317. display: block;
  318. }}
  319. .search-word-item {{
  320. padding: 12px 20px 12px 50px;
  321. cursor: pointer;
  322. border-left: 3px solid transparent;
  323. transition: all 0.2s;
  324. }}
  325. .search-word-item:hover {{
  326. background: #f9fafb;
  327. border-left-color: #667eea;
  328. }}
  329. .search-word-item.active {{
  330. background: #ede9fe;
  331. border-left-color: #7c3aed;
  332. }}
  333. .search-word-text {{
  334. font-size: 14px;
  335. font-weight: 500;
  336. color: #374151;
  337. margin-bottom: 4px;
  338. }}
  339. .search-word-score {{
  340. display: inline-block;
  341. padding: 2px 8px;
  342. border-radius: 12px;
  343. font-size: 11px;
  344. font-weight: 600;
  345. margin-left: 8px;
  346. }}
  347. .score-high {{
  348. background: #d1fae5;
  349. color: #065f46;
  350. }}
  351. .score-medium {{
  352. background: #fef3c7;
  353. color: #92400e;
  354. }}
  355. .score-low {{
  356. background: #fee2e2;
  357. color: #991b1b;
  358. }}
  359. /* 评估徽章 */
  360. .eval-badge {{
  361. display: inline-block;
  362. padding: 2px 6px;
  363. border-radius: 10px;
  364. font-size: 11px;
  365. font-weight: 600;
  366. margin-left: 6px;
  367. }}
  368. .eval-complete {{
  369. background: #d1fae5;
  370. color: #065f46;
  371. border: 1px solid #10b981;
  372. }}
  373. .eval-similar {{
  374. background: #fef3c7;
  375. color: #92400e;
  376. border: 1px solid #f59e0b;
  377. }}
  378. .eval-weak {{
  379. background: #fed7aa;
  380. color: #9a3412;
  381. border: 1px solid #f97316;
  382. }}
  383. .eval-none {{
  384. background: #fee2e2;
  385. color: #991b1b;
  386. border: 1px solid #ef4444;
  387. }}
  388. .eval-filtered {{
  389. background: #e5e7eb;
  390. color: #4b5563;
  391. border: 1px solid #6b7280;
  392. }}
  393. .search-word-eval {{
  394. font-size: 11px;
  395. color: #6b7280;
  396. margin-top: 4px;
  397. }}
  398. /* 右侧结果区 */
  399. .right-content {{
  400. flex: 1;
  401. overflow-y: auto;
  402. padding-bottom: 40px;
  403. }}
  404. .result-block {{
  405. background: white;
  406. border-radius: 8px;
  407. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  408. margin-bottom: 30px;
  409. padding: 20px;
  410. scroll-margin-top: 20px;
  411. }}
  412. .result-header {{
  413. margin-bottom: 20px;
  414. padding-bottom: 15px;
  415. border-bottom: 2px solid #e5e7eb;
  416. }}
  417. .result-title {{
  418. font-size: 20px;
  419. font-weight: 600;
  420. color: #111827;
  421. margin-bottom: 10px;
  422. }}
  423. .result-stats {{
  424. display: flex;
  425. gap: 10px;
  426. font-size: 12px;
  427. color: #6b7280;
  428. flex-wrap: wrap;
  429. }}
  430. .stat-badge {{
  431. background: #f3f4f6;
  432. padding: 4px 10px;
  433. border-radius: 4px;
  434. }}
  435. .stat-badge.eval {{
  436. font-weight: 600;
  437. }}
  438. .stat-badge.eval.complete {{
  439. background: #d1fae5;
  440. color: #065f46;
  441. }}
  442. .stat-badge.eval.similar {{
  443. background: #fef3c7;
  444. color: #92400e;
  445. }}
  446. .stat-badge.eval.weak {{
  447. background: #fed7aa;
  448. color: #9a3412;
  449. }}
  450. .stat-badge.eval.none {{
  451. background: #fee2e2;
  452. color: #991b1b;
  453. }}
  454. .stat-badge.eval.filtered {{
  455. background: #e5e7eb;
  456. color: #4b5563;
  457. }}
  458. /* 帖子网格 */
  459. .notes-grid {{
  460. display: grid;
  461. grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  462. gap: 20px;
  463. }}
  464. .note-card {{
  465. border: 3px solid #e5e7eb;
  466. border-radius: 8px;
  467. overflow: hidden;
  468. cursor: pointer;
  469. transition: all 0.3s;
  470. background: white;
  471. }}
  472. .note-card:hover {{
  473. transform: translateY(-4px);
  474. box-shadow: 0 10px 25px rgba(0,0,0,0.15);
  475. }}
  476. /* 根据评估分数设置边框颜色 */
  477. .note-card.eval-complete {{
  478. border-color: #10b981;
  479. }}
  480. .note-card.eval-similar {{
  481. border-color: #f59e0b;
  482. }}
  483. .note-card.eval-weak {{
  484. border-color: #f97316;
  485. }}
  486. .note-card.eval-none {{
  487. border-color: #ef4444;
  488. }}
  489. .note-card.eval-filtered {{
  490. border-color: #6b7280;
  491. opacity: 0.6;
  492. }}
  493. /* 图片轮播 */
  494. .image-carousel {{
  495. position: relative;
  496. width: 100%;
  497. height: 280px;
  498. background: #f3f4f6;
  499. overflow: hidden;
  500. }}
  501. .carousel-images {{
  502. display: flex;
  503. height: 100%;
  504. transition: transform 0.3s ease;
  505. }}
  506. .carousel-image {{
  507. min-width: 100%;
  508. height: 100%;
  509. object-fit: cover;
  510. }}
  511. .carousel-btn {{
  512. position: absolute;
  513. top: 50%;
  514. transform: translateY(-50%);
  515. background: rgba(0,0,0,0.5);
  516. color: white;
  517. border: none;
  518. width: 32px;
  519. height: 32px;
  520. border-radius: 50%;
  521. cursor: pointer;
  522. font-size: 16px;
  523. display: none;
  524. align-items: center;
  525. justify-content: center;
  526. transition: background 0.2s;
  527. z-index: 10;
  528. }}
  529. .carousel-btn:hover {{
  530. background: rgba(0,0,0,0.7);
  531. }}
  532. .carousel-btn.prev {{
  533. left: 8px;
  534. }}
  535. .carousel-btn.next {{
  536. right: 8px;
  537. }}
  538. .note-card:hover .carousel-btn {{
  539. display: flex;
  540. }}
  541. .carousel-indicators {{
  542. position: absolute;
  543. bottom: 10px;
  544. left: 50%;
  545. transform: translateX(-50%);
  546. display: flex;
  547. gap: 6px;
  548. z-index: 10;
  549. }}
  550. .dot {{
  551. width: 8px;
  552. height: 8px;
  553. border-radius: 50%;
  554. background: rgba(255,255,255,0.5);
  555. cursor: pointer;
  556. transition: all 0.2s;
  557. }}
  558. .dot.active {{
  559. background: white;
  560. width: 24px;
  561. border-radius: 4px;
  562. }}
  563. .image-counter {{
  564. position: absolute;
  565. top: 10px;
  566. right: 10px;
  567. background: rgba(0,0,0,0.6);
  568. color: white;
  569. padding: 4px 8px;
  570. border-radius: 4px;
  571. font-size: 12px;
  572. z-index: 10;
  573. }}
  574. /* 帖子信息 */
  575. .note-info {{
  576. padding: 12px;
  577. }}
  578. .note-title {{
  579. font-size: 14px;
  580. font-weight: 500;
  581. color: #111827;
  582. margin-bottom: 8px;
  583. display: -webkit-box;
  584. -webkit-line-clamp: 2;
  585. -webkit-box-orient: vertical;
  586. overflow: hidden;
  587. line-height: 1.4;
  588. }}
  589. .note-meta {{
  590. display: flex;
  591. align-items: center;
  592. justify-content: space-between;
  593. font-size: 12px;
  594. color: #6b7280;
  595. margin-bottom: 8px;
  596. }}
  597. .note-type {{
  598. padding: 3px 8px;
  599. border-radius: 4px;
  600. font-weight: 500;
  601. }}
  602. .type-video {{
  603. background: #dbeafe;
  604. color: #1e40af;
  605. }}
  606. .type-normal {{
  607. background: #d1fae5;
  608. color: #065f46;
  609. }}
  610. .note-author {{
  611. display: flex;
  612. align-items: center;
  613. gap: 6px;
  614. }}
  615. .author-avatar {{
  616. width: 24px;
  617. height: 24px;
  618. border-radius: 50%;
  619. }}
  620. /* 评估信息 */
  621. .note-eval {{
  622. padding: 8px 12px;
  623. background: #f9fafb;
  624. border-top: 1px solid #e5e7eb;
  625. font-size: 12px;
  626. }}
  627. .note-eval-header {{
  628. display: flex;
  629. align-items: center;
  630. justify-content: space-between;
  631. cursor: pointer;
  632. user-select: none;
  633. }}
  634. .note-eval-score {{
  635. font-weight: 600;
  636. }}
  637. .note-eval-toggle {{
  638. color: #6b7280;
  639. font-size: 10px;
  640. }}
  641. .note-eval-details {{
  642. margin-top: 8px;
  643. padding-top: 8px;
  644. border-top: 1px solid #e5e7eb;
  645. display: none;
  646. line-height: 1.5;
  647. }}
  648. .note-eval-details.expanded {{
  649. display: block;
  650. }}
  651. .eval-detail-label {{
  652. font-weight: 600;
  653. color: #374151;
  654. margin-top: 6px;
  655. margin-bottom: 2px;
  656. }}
  657. .eval-detail-label:first-child {{
  658. margin-top: 0;
  659. }}
  660. .eval-detail-text {{
  661. color: #6b7280;
  662. }}
  663. /* 滚动条样式 */
  664. ::-webkit-scrollbar {{
  665. width: 8px;
  666. height: 8px;
  667. }}
  668. ::-webkit-scrollbar-track {{
  669. background: #f1f1f1;
  670. }}
  671. ::-webkit-scrollbar-thumb {{
  672. background: #888;
  673. border-radius: 4px;
  674. }}
  675. ::-webkit-scrollbar-thumb:hover {{
  676. background: #555;
  677. }}
  678. /* 隐藏类 */
  679. .hidden {{
  680. display: none !important;
  681. }}
  682. </style>
  683. </head>
  684. <body>
  685. <!-- 统计面板 -->
  686. <div class="stats-panel">
  687. <div class="stats-container">
  688. <div class="stats-row">
  689. <div class="stat-item">
  690. <div class="stat-value">📊 {stats['total_features']}</div>
  691. <div class="stat-label">原始特征数</div>
  692. </div>
  693. <div class="stat-item">
  694. <div class="stat-value">🔍 {stats['total_search_words']}</div>
  695. <div class="stat-label">搜索词数</div>
  696. </div>
  697. <div class="stat-item">
  698. <div class="stat-value">📝 {stats['total_notes']}</div>
  699. <div class="stat-label">帖子总数</div>
  700. </div>
  701. <div class="stat-item">
  702. <div class="stat-value">🎬 {stats['video_count']}</div>
  703. <div class="stat-label">视频类型 ({stats['video_percentage']}%)</div>
  704. </div>
  705. <div class="stat-item">
  706. <div class="stat-value">📷 {stats['normal_count']}</div>
  707. <div class="stat-label">图文类型 ({stats['normal_percentage']}%)</div>
  708. </div>
  709. </div>
  710. <div class="stats-row">
  711. <div class="stat-item small">
  712. <div class="stat-value">⚡ {stats['total_evaluated']}</div>
  713. <div class="stat-label">已评估</div>
  714. </div>
  715. <div class="stat-item small">
  716. <div class="stat-value">⚫ {stats['total_filtered']}</div>
  717. <div class="stat-label">已过滤 ({stats['filter_rate']}%)</div>
  718. </div>
  719. <div class="stat-item small">
  720. <div class="stat-value">🟢 {stats['match_complete']}</div>
  721. <div class="stat-label">完全匹配 ({stats['complete_rate']}%)</div>
  722. </div>
  723. <div class="stat-item small">
  724. <div class="stat-value">🟡 {stats['match_similar']}</div>
  725. <div class="stat-label">相似匹配 ({stats['similar_rate']}%)</div>
  726. </div>
  727. <div class="stat-item small">
  728. <div class="stat-value">🟠 {stats['match_weak']}</div>
  729. <div class="stat-label">弱相似</div>
  730. </div>
  731. <div class="stat-item small">
  732. <div class="stat-value">🔴 {stats['match_none']}</div>
  733. <div class="stat-label">无匹配</div>
  734. </div>
  735. </div>
  736. </div>
  737. </div>
  738. <!-- 过滤控制面板 -->
  739. <div class="filter-panel">
  740. <span class="filter-label">🔍 筛选显示:</span>
  741. <div class="filter-buttons">
  742. <button class="filter-btn active" onclick="filterNotes('all')">全部</button>
  743. <button class="filter-btn complete" onclick="filterNotes('complete')">🟢 完全匹配</button>
  744. <button class="filter-btn similar" onclick="filterNotes('similar')">🟡 相似匹配</button>
  745. <button class="filter-btn weak" onclick="filterNotes('weak')">🟠 弱相似</button>
  746. <button class="filter-btn none" onclick="filterNotes('none')">🔴 无匹配</button>
  747. <button class="filter-btn filtered" onclick="filterNotes('filtered')">⚫ 已过滤</button>
  748. </div>
  749. </div>
  750. <!-- 主容器 -->
  751. <div class="main-container">
  752. <!-- 左侧导航 -->
  753. <div class="left-sidebar" id="leftSidebar">
  754. <!-- 通过JavaScript动态生成 -->
  755. </div>
  756. <!-- 右侧结果区 -->
  757. <div class="right-content" id="rightContent">
  758. <!-- 通过JavaScript动态生成 -->
  759. </div>
  760. </div>
  761. <script>
  762. // 数据
  763. const data = {data_json};
  764. let currentFilter = 'all';
  765. // 创建评估映射(使用索引: "featureIdx-groupIdx-swIdx-noteIdx" -> evaluation)
  766. const noteEvaluations = {{}};
  767. data.forEach((feature, fIdx) => {{
  768. const groups = feature['组合评估结果_分组'] || [];
  769. groups.forEach((group, gIdx) => {{
  770. const searches = group['top10_searches'] || [];
  771. searches.forEach((search, sIdx) => {{
  772. const evaluation = search['evaluation_with_filter'];
  773. if (evaluation && evaluation.notes_evaluation) {{
  774. evaluation.notes_evaluation.forEach(noteEval => {{
  775. const key = `${{fIdx}}-${{gIdx}}-${{sIdx}}-${{noteEval.note_index}}`;
  776. noteEvaluations[key] = noteEval;
  777. }});
  778. }}
  779. }});
  780. }});
  781. }});
  782. // 获取评估类别
  783. function getEvalCategory(noteEval) {{
  784. if (!noteEval || noteEval['Query相关性'] !== '相关') {{
  785. return 'filtered';
  786. }}
  787. const score = noteEval['综合得分'];
  788. if (score >= 8) return 'complete';
  789. if (score >= 6) return 'similar';
  790. if (score >= 5) return 'weak';
  791. return 'none';
  792. }}
  793. // 渲染左侧导航
  794. function renderLeftSidebar() {{
  795. const sidebar = document.getElementById('leftSidebar');
  796. let html = '';
  797. data.forEach((feature, featureIdx) => {{
  798. const groups = feature['组合评估结果_分组'] || [];
  799. let totalSearches = 0;
  800. groups.forEach(group => {{
  801. totalSearches += (group['top10_searches'] || []).length;
  802. }});
  803. // 层级1: 原始特征
  804. html += `
  805. <div class="feature-group">
  806. <div class="feature-header" onclick="toggleFeature(${{featureIdx}})" id="feature-header-${{featureIdx}}">
  807. <div class="feature-title">${{feature['原始特征名称']}}</div>
  808. <div class="feature-meta">
  809. ${{feature['来源层级']}} · 权重: ${{feature['权重'].toFixed(2)}} · ${{totalSearches}}个搜索词
  810. </div>
  811. </div>
  812. <div class="search-words-list" id="search-words-${{featureIdx}}">
  813. `;
  814. // 层级2: Base word分组
  815. groups.forEach((group, groupIdx) => {{
  816. const baseWord = group['base_word'] || '';
  817. const baseSimilarity = group['base_word_similarity'] || 0;
  818. const searches = group['top10_searches'] || [];
  819. // 获取相关词汇
  820. const relatedWords = feature['高相似度候选_按base_word']?.[baseWord] || [];
  821. const relatedWordNames = relatedWords.map(w => w['人设特征名称']).slice(0, 10).join('、');
  822. html += `
  823. <div class="base-word-group">
  824. <div class="base-word-header" onclick="toggleBaseWord(${{featureIdx}}, ${{groupIdx}})"
  825. id="base-word-header-${{featureIdx}}-${{groupIdx}}">
  826. <div class="base-word-title">🎯 ${{baseWord}}</div>
  827. <div class="base-word-meta">相似度: ${{baseSimilarity.toFixed(2)}} · ${{searches.length}}个搜索词</div>
  828. </div>
  829. <div class="base-word-desc" id="base-word-desc-${{featureIdx}}-${{groupIdx}}">
  830. ${{relatedWordNames || '无相关词汇'}}
  831. </div>
  832. <div class="search-words-sublist" id="search-words-sublist-${{featureIdx}}-${{groupIdx}}">
  833. `;
  834. // 层级3: 搜索词列表
  835. searches.forEach((sw, swIdx) => {{
  836. const score = sw.score || 0;
  837. const scoreClass = score >= 0.9 ? 'score-high' : score >= 0.7 ? 'score-medium' : 'score-low';
  838. const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
  839. const sourceWord = sw.source_word || '';
  840. // 获取评估统计
  841. const evaluation = sw['evaluation_with_filter'];
  842. let evalBadges = '';
  843. if (evaluation) {{
  844. const stats = evaluation.statistics || {{}};
  845. const complete = stats['完全匹配(8-10)'] || 0;
  846. const similar = stats['相似匹配(6-7)'] || 0;
  847. const weak = stats['弱相似(5-6)'] || 0;
  848. const none = stats['无匹配(≤4)'] || 0;
  849. const filtered = evaluation.filtered_count || 0;
  850. if (complete > 0) evalBadges += `<span class="eval-badge eval-complete">🟢${{complete}}</span>`;
  851. if (similar > 0) evalBadges += `<span class="eval-badge eval-similar">🟡${{similar}}</span>`;
  852. if (weak > 0) evalBadges += `<span class="eval-badge eval-weak">🟠${{weak}}</span>`;
  853. if (none > 0) evalBadges += `<span class="eval-badge eval-none">🔴${{none}}</span>`;
  854. if (filtered > 0) evalBadges += `<span class="eval-badge eval-filtered">⚫${{filtered}}</span>`;
  855. }}
  856. html += `
  857. <div class="search-word-item" onclick="scrollToBlock('${{blockId}}')"
  858. id="sw-${{featureIdx}}-${{groupIdx}}-${{swIdx}}"
  859. data-block-id="${{blockId}}">
  860. <div class="search-word-text">
  861. 🔍 ${{sw.search_word}}
  862. <span class="search-word-score ${{scoreClass}}">${{score.toFixed(2)}}</span>
  863. </div>
  864. <div class="search-word-meta" style="font-size:11px;color:#9ca3af;margin-top:2px">
  865. 来源: ${{sourceWord}}
  866. </div>
  867. <div class="search-word-eval">${{evalBadges}}</div>
  868. </div>
  869. `;
  870. }});
  871. html += `
  872. </div>
  873. </div>
  874. `;
  875. }});
  876. html += `
  877. </div>
  878. </div>
  879. `;
  880. }});
  881. sidebar.innerHTML = html;
  882. }}
  883. // 渲染右侧结果区
  884. function renderRightContent() {{
  885. const content = document.getElementById('rightContent');
  886. let html = '';
  887. data.forEach((feature, featureIdx) => {{
  888. const groups = feature['组合评估结果_分组'] || [];
  889. groups.forEach((group, groupIdx) => {{
  890. const searches = group['top10_searches'] || [];
  891. searches.forEach((sw, swIdx) => {{
  892. const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
  893. const searchResult = sw.search_result || {{}};
  894. const notes = searchResult.data?.data || [];
  895. const videoCount = notes.filter(n => n.note_card?.type === 'video').length;
  896. const normalCount = notes.length - videoCount;
  897. // 获取评估统计
  898. const evaluation = sw['evaluation_with_filter'];
  899. let evalStats = '';
  900. if (evaluation) {{
  901. const stats = evaluation.statistics || {{}};
  902. const complete = stats['完全匹配(8-10)'] || 0;
  903. const similar = stats['相似匹配(6-7)'] || 0;
  904. const weak = stats['弱相似(5-6)'] || 0;
  905. const none = stats['无匹配(≤4)'] || 0;
  906. const filtered = evaluation.filtered_count || 0;
  907. if (complete > 0) evalStats += `<span class="stat-badge eval complete">🟢 完全:${{complete}}</span>`;
  908. if (similar > 0) evalStats += `<span class="stat-badge eval similar">🟡 相似:${{similar}}</span>`;
  909. if (weak > 0) evalStats += `<span class="stat-badge eval weak">🟠 弱:${{weak}}</span>`;
  910. if (none > 0) evalStats += `<span class="stat-badge eval none">🔴 无:${{none}}</span>`;
  911. if (filtered > 0) evalStats += `<span class="stat-badge eval filtered">⚫ 过滤:${{filtered}}</span>`;
  912. }}
  913. html += `
  914. <div class="result-block" id="${{blockId}}">
  915. <div class="result-header">
  916. <div class="result-title">${{sw.search_word}}</div>
  917. <div class="result-stats">
  918. <span class="stat-badge">📝 ${{notes.length}} 条帖子</span>
  919. <span class="stat-badge">🎬 ${{videoCount}} 视频</span>
  920. <span class="stat-badge">📷 ${{normalCount}} 图文</span>
  921. ${{evalStats}}
  922. </div>
  923. </div>
  924. <div class="notes-grid">
  925. ${{notes.map((note, noteIdx) => renderNoteCard(note, featureIdx, groupIdx, swIdx, noteIdx)).join('')}}
  926. </div>
  927. </div>
  928. `;
  929. }});
  930. }});
  931. }});
  932. content.innerHTML = html;
  933. }}
  934. // 渲染单个帖子卡片
  935. function renderNoteCard(note, featureIdx, groupIdx, swIdx, noteIdx) {{
  936. const card = note.note_card || {{}};
  937. const images = card.image_list || [];
  938. const title = card.display_title || '无标题';
  939. const noteType = card.type || 'normal';
  940. const noteId = note.id || '';
  941. const user = card.user || {{}};
  942. const userName = user.nick_name || '未知用户';
  943. const userAvatar = user.avatar || '';
  944. const carouselId = `carousel-${{featureIdx}}-${{groupIdx}}-${{swIdx}}-${{noteIdx}}`;
  945. // 获取评估结果(使用索引key)
  946. const evalKey = `${{featureIdx}}-${{groupIdx}}-${{swIdx}}-${{noteIdx}}`;
  947. const noteEval = noteEvaluations[evalKey];
  948. const evalCategory = getEvalCategory(noteEval);
  949. const evalClass = `eval-${{evalCategory}}`;
  950. let evalSection = '';
  951. if (noteEval) {{
  952. const score = noteEval['综合得分'];
  953. const scoreEmoji = score >= 8 ? '🟢' : score >= 6 ? '🟡' : score >= 5 ? '🟠' : '🔴';
  954. const scoreText = score >= 8 ? '完全匹配' : score >= 6 ? '相似匹配' : score >= 5 ? '弱相似' : '无匹配';
  955. const reasoning = noteEval['评分说明'] || '无';
  956. const matchingPoints = (noteEval['关键匹配点'] || []).join('、') || '无';
  957. evalSection = `
  958. <div class="note-eval">
  959. <div class="note-eval-header" onclick="event.stopPropagation(); toggleEvalDetails('${{carouselId}}')">
  960. <span class="note-eval-score">${{scoreEmoji}} ${{scoreText}} (${{score}}分)</span>
  961. <span class="note-eval-toggle" id="${{carouselId}}-toggle">▼ 详情</span>
  962. </div>
  963. <div class="note-eval-details" id="${{carouselId}}-details">
  964. <div class="eval-detail-label">评估理由:</div>
  965. <div class="eval-detail-text">${{reasoning}}</div>
  966. <div class="eval-detail-label">匹配要点:</div>
  967. <div class="eval-detail-text">${{matchingPoints}}</div>
  968. </div>
  969. </div>
  970. `;
  971. }} else if (evalCategory === 'filtered') {{
  972. evalSection = `
  973. <div class="note-eval">
  974. <div class="note-eval-score">⚫ 已过滤(与搜索无关)</div>
  975. </div>
  976. `;
  977. }}
  978. return `
  979. <div class="note-card ${{evalClass}}" data-eval-category="${{evalCategory}}" onclick="openNote('${{noteId}}')">
  980. <div class="image-carousel" id="${{carouselId}}">
  981. <div class="carousel-images">
  982. ${{images.map(img => `<img class="carousel-image" src="${{img}}" alt="帖子图片" loading="lazy">`).join('')}}
  983. </div>
  984. ${{images.length > 1 ? `
  985. <button class="carousel-btn prev" onclick="event.stopPropagation(); changeImage('${{carouselId}}', -1)">←</button>
  986. <button class="carousel-btn next" onclick="event.stopPropagation(); changeImage('${{carouselId}}', 1)">→</button>
  987. <div class="carousel-indicators">
  988. ${{images.map((_, i) => `<span class="dot ${{i === 0 ? 'active' : ''}}" onclick="event.stopPropagation(); goToImage('${{carouselId}}', ${{i}})"></span>`).join('')}}
  989. </div>
  990. <span class="image-counter">1/${{images.length}}</span>
  991. ` : ''}}
  992. </div>
  993. <div class="note-info">
  994. <div class="note-title">${{title}}</div>
  995. <div class="note-meta">
  996. <span class="note-type type-${{noteType}}">
  997. ${{noteType === 'video' ? '🎬 视频' : '📷 图文'}}
  998. </span>
  999. <div class="note-author">
  1000. ${{userAvatar ? `<img class="author-avatar" src="${{userAvatar}}" alt="${{userName}}">` : ''}}
  1001. <span>${{userName}}</span>
  1002. </div>
  1003. </div>
  1004. </div>
  1005. ${{evalSection}}
  1006. </div>
  1007. `;
  1008. }}
  1009. // 图片轮播逻辑
  1010. const carouselStates = {{}};
  1011. function changeImage(carouselId, direction) {{
  1012. if (!carouselStates[carouselId]) {{
  1013. carouselStates[carouselId] = {{ currentIndex: 0 }};
  1014. }}
  1015. const carousel = document.getElementById(carouselId);
  1016. const imagesContainer = carousel.querySelector('.carousel-images');
  1017. const images = carousel.querySelectorAll('.carousel-image');
  1018. const dots = carousel.querySelectorAll('.dot');
  1019. const counter = carousel.querySelector('.image-counter');
  1020. let newIndex = carouselStates[carouselId].currentIndex + direction;
  1021. if (newIndex < 0) newIndex = images.length - 1;
  1022. if (newIndex >= images.length) newIndex = 0;
  1023. carouselStates[carouselId].currentIndex = newIndex;
  1024. imagesContainer.style.transform = `translateX(-${{newIndex * 100}}%)`;
  1025. // 更新指示器
  1026. dots.forEach((dot, i) => {{
  1027. dot.classList.toggle('active', i === newIndex);
  1028. }});
  1029. // 更新计数器
  1030. if (counter) {{
  1031. counter.textContent = `${{newIndex + 1}}/${{images.length}}`;
  1032. }}
  1033. }}
  1034. function goToImage(carouselId, index) {{
  1035. if (!carouselStates[carouselId]) {{
  1036. carouselStates[carouselId] = {{ currentIndex: 0 }};
  1037. }}
  1038. const carousel = document.getElementById(carouselId);
  1039. const imagesContainer = carousel.querySelector('.carousel-images');
  1040. const dots = carousel.querySelectorAll('.dot');
  1041. const counter = carousel.querySelector('.image-counter');
  1042. carouselStates[carouselId].currentIndex = index;
  1043. imagesContainer.style.transform = `translateX(-${{index * 100}}%)`;
  1044. // 更新指示器
  1045. dots.forEach((dot, i) => {{
  1046. dot.classList.toggle('active', i === index);
  1047. }});
  1048. // 更新计数器
  1049. if (counter) {{
  1050. counter.textContent = `${{index + 1}}/${{dots.length}}`;
  1051. }}
  1052. }}
  1053. // 展开/折叠特征组
  1054. function toggleFeature(featureIdx) {{
  1055. const searchWordsList = document.getElementById(`search-words-${{featureIdx}}`);
  1056. const featureHeader = document.getElementById(`feature-header-${{featureIdx}}`);
  1057. searchWordsList.classList.toggle('expanded');
  1058. featureHeader.classList.toggle('active');
  1059. }}
  1060. // 展开/折叠base word分组
  1061. function toggleBaseWord(featureIdx, groupIdx) {{
  1062. const baseWordHeader = document.getElementById(`base-word-header-${{featureIdx}}-${{groupIdx}}`);
  1063. const baseWordDesc = document.getElementById(`base-word-desc-${{featureIdx}}-${{groupIdx}}`);
  1064. const searchWordsSublist = document.getElementById(`search-words-sublist-${{featureIdx}}-${{groupIdx}}`);
  1065. baseWordHeader.classList.toggle('active');
  1066. baseWordDesc.classList.toggle('expanded');
  1067. searchWordsSublist.classList.toggle('expanded');
  1068. }}
  1069. // 滚动到指定结果块
  1070. function scrollToBlock(blockId) {{
  1071. const block = document.getElementById(blockId);
  1072. if (block) {{
  1073. block.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  1074. // 高亮对应的搜索词
  1075. document.querySelectorAll('.search-word-item').forEach(item => {{
  1076. item.classList.remove('active');
  1077. }});
  1078. document.querySelectorAll(`[data-block-id="${{blockId}}"]`).forEach(item => {{
  1079. item.classList.add('active');
  1080. }});
  1081. }}
  1082. }}
  1083. // 切换评估详情
  1084. function toggleEvalDetails(carouselId) {{
  1085. const details = document.getElementById(`${{carouselId}}-details`);
  1086. const toggle = document.getElementById(`${{carouselId}}-toggle`);
  1087. if (details && toggle) {{
  1088. details.classList.toggle('expanded');
  1089. toggle.textContent = details.classList.contains('expanded') ? '▲ 收起' : '▼ 详情';
  1090. }}
  1091. }}
  1092. // 过滤帖子
  1093. function filterNotes(category) {{
  1094. currentFilter = category;
  1095. // 更新按钮状态
  1096. document.querySelectorAll('.filter-btn').forEach(btn => {{
  1097. btn.classList.remove('active');
  1098. }});
  1099. event.target.classList.add('active');
  1100. // 过滤帖子卡片
  1101. document.querySelectorAll('.note-card').forEach(card => {{
  1102. const evalCategory = card.getAttribute('data-eval-category');
  1103. if (category === 'all' || evalCategory === category) {{
  1104. card.classList.remove('hidden');
  1105. }} else {{
  1106. card.classList.add('hidden');
  1107. }}
  1108. }});
  1109. // 隐藏空的结果块
  1110. document.querySelectorAll('.result-block').forEach(block => {{
  1111. const visibleCards = block.querySelectorAll('.note-card:not(.hidden)');
  1112. if (visibleCards.length === 0) {{
  1113. block.classList.add('hidden');
  1114. }} else {{
  1115. block.classList.remove('hidden');
  1116. }}
  1117. }});
  1118. }}
  1119. // 打开小红书帖子
  1120. function openNote(noteId) {{
  1121. if (noteId) {{
  1122. window.open(`https://www.xiaohongshu.com/explore/${{noteId}}`, '_blank');
  1123. }}
  1124. }}
  1125. // 初始化
  1126. document.addEventListener('DOMContentLoaded', () => {{
  1127. renderLeftSidebar();
  1128. renderRightContent();
  1129. // 默认展开第一个特征组和第一个base_word
  1130. if (data.length > 0) {{
  1131. toggleFeature(0);
  1132. // 展开第一个base_word分组
  1133. const firstGroups = data[0]['组合评估结果_分组'];
  1134. if (firstGroups && firstGroups.length > 0) {{
  1135. toggleBaseWord(0, 0);
  1136. }}
  1137. }}
  1138. }});
  1139. </script>
  1140. </body>
  1141. </html>
  1142. '''
  1143. # 写入文件
  1144. with open(output_path, 'w', encoding='utf-8') as f:
  1145. f.write(html_content)
  1146. def main():
  1147. """主函数"""
  1148. # 配置路径
  1149. script_dir = os.path.dirname(os.path.abspath(__file__))
  1150. json_path = os.path.join(script_dir, 'output_v2', 'stage6_with_evaluations.json')
  1151. output_dir = os.path.join(script_dir, 'visualization')
  1152. os.makedirs(output_dir, exist_ok=True)
  1153. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  1154. output_path = os.path.join(output_dir, f'stage6_interactive_{timestamp}.html')
  1155. # 加载数据
  1156. print(f"📖 加载数据: {json_path}")
  1157. data = load_data(json_path)
  1158. print(f"✓ 加载了 {len(data)} 个原始特征")
  1159. # 计算统计
  1160. print("📊 计算统计数据...")
  1161. stats = calculate_statistics(data)
  1162. print(f"✓ 统计完成:")
  1163. print(f" - 原始特征: {stats['total_features']}")
  1164. print(f" - 搜索词: {stats['total_search_words']}")
  1165. print(f" - 帖子总数: {stats['total_notes']}")
  1166. print(f" - 视频: {stats['video_count']} ({stats['video_percentage']}%)")
  1167. print(f" - 图文: {stats['normal_count']} ({stats['normal_percentage']}%)")
  1168. print(f"\n 评估结果:")
  1169. print(f" - 已评估: {stats['total_evaluated']}")
  1170. print(f" - 已过滤: {stats['total_filtered']} ({stats['filter_rate']}%)")
  1171. print(f" - 完全匹配: {stats['match_complete']} ({stats['complete_rate']}%)")
  1172. print(f" - 相似匹配: {stats['match_similar']} ({stats['similar_rate']}%)")
  1173. print(f" - 弱相似: {stats['match_weak']}")
  1174. print(f" - 无匹配: {stats['match_none']}")
  1175. # 生成HTML
  1176. print(f"\n🎨 生成可视化页面...")
  1177. generate_html(data, stats, output_path)
  1178. print(f"✓ 生成完成: {output_path}")
  1179. # 打印访问提示
  1180. print(f"\n🌐 在浏览器中打开查看:")
  1181. print(f" file://{output_path}")
  1182. return output_path
  1183. if __name__ == '__main__':
  1184. main()