bilisum 1.13.2 → 1.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +4 -4
  2. package/bin/bilisum.js +157 -76
  3. package/package.json +7 -2
  4. package/runtime/VERSION +1 -0
  5. package/runtime/apps/service/pyproject.toml +29 -0
  6. package/runtime/apps/service/src/video_sum_service/__init__.py +1 -0
  7. package/runtime/apps/service/src/video_sum_service/__main__.py +5 -0
  8. package/runtime/apps/service/src/video_sum_service/app.py +480 -0
  9. package/runtime/apps/service/src/video_sum_service/context.py +24 -0
  10. package/runtime/apps/service/src/video_sum_service/integrations.py +264 -0
  11. package/runtime/apps/service/src/video_sum_service/knowledge/__init__.py +5 -0
  12. package/runtime/apps/service/src/video_sum_service/knowledge/index_service.py +408 -0
  13. package/runtime/apps/service/src/video_sum_service/knowledge/local_llm.py +319 -0
  14. package/runtime/apps/service/src/video_sum_service/knowledge/rag_service.py +492 -0
  15. package/runtime/apps/service/src/video_sum_service/knowledge/tag_service.py +242 -0
  16. package/runtime/apps/service/src/video_sum_service/main.py +31 -0
  17. package/runtime/apps/service/src/video_sum_service/repository.py +942 -0
  18. package/runtime/apps/service/src/video_sum_service/routers/__init__.py +1 -0
  19. package/runtime/apps/service/src/video_sum_service/routers/knowledge.py +272 -0
  20. package/runtime/apps/service/src/video_sum_service/routers/system.py +280 -0
  21. package/runtime/apps/service/src/video_sum_service/routers/tasks.py +287 -0
  22. package/runtime/apps/service/src/video_sum_service/routers/videos.py +766 -0
  23. package/runtime/apps/service/src/video_sum_service/runtime_support.py +1007 -0
  24. package/runtime/apps/service/src/video_sum_service/schemas.py +418 -0
  25. package/runtime/apps/service/src/video_sum_service/settings_manager.py +130 -0
  26. package/runtime/apps/service/src/video_sum_service/task_artifacts.py +102 -0
  27. package/runtime/apps/service/src/video_sum_service/task_exports.py +108 -0
  28. package/runtime/apps/service/src/video_sum_service/transcribe_worker.py +9 -0
  29. package/runtime/apps/service/src/video_sum_service/video_assets.py +623 -0
  30. package/runtime/apps/service/src/video_sum_service/worker.py +381 -0
  31. package/runtime/apps/web/static/apple-touch-icon.png +0 -0
  32. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  33. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  34. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  35. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  36. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  37. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  38. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  39. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  40. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  41. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  42. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  43. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  44. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  45. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  46. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  47. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  48. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  49. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  50. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  51. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  52. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  53. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  54. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  55. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  56. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  57. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  58. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  59. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  60. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  61. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  62. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  63. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  64. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  65. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  66. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  67. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  68. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  69. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  70. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  71. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  72. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  73. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  74. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  75. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  76. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  77. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  78. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  79. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  80. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  81. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  82. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  83. package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  84. package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  85. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  86. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  87. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  88. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  89. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  90. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  91. package/runtime/apps/web/static/assets/icons/icon-180.png +0 -0
  92. package/runtime/apps/web/static/assets/icons/icon-512.png +0 -0
  93. package/runtime/apps/web/static/assets/icons/icon.svg +17 -0
  94. package/runtime/apps/web/static/assets/index-Cj5PVjQg.js +388 -0
  95. package/runtime/apps/web/static/assets/index-DU2t4_7s.css +1 -0
  96. package/runtime/apps/web/static/favicon-32x32.png +0 -0
  97. package/runtime/apps/web/static/favicon.ico +0 -0
  98. package/runtime/apps/web/static/favicon.svg +17 -0
  99. package/runtime/apps/web/static/index.html +19 -0
  100. package/runtime/apps/web/static/js/api.js +123 -0
  101. package/runtime/apps/web/static/js/main.js +932 -0
  102. package/runtime/apps/web/static/js/state.js +28 -0
  103. package/runtime/apps/web/static/js/utils.js +81 -0
  104. package/runtime/apps/web/static/js/views/home.js +604 -0
  105. package/runtime/apps/web/static/js/views/settings.js +477 -0
  106. package/runtime/apps/web/static/styles.css +2206 -0
  107. package/runtime/packages/core/pyproject.toml +18 -0
  108. package/runtime/packages/core/src/video_sum_core/__init__.py +1 -0
  109. package/runtime/packages/core/src/video_sum_core/errors.py +22 -0
  110. package/runtime/packages/core/src/video_sum_core/markdown_exports.py +154 -0
  111. package/runtime/packages/core/src/video_sum_core/models/__init__.py +1 -0
  112. package/runtime/packages/core/src/video_sum_core/models/tasks.py +70 -0
  113. package/runtime/packages/core/src/video_sum_core/pipeline/__init__.py +1 -0
  114. package/runtime/packages/core/src/video_sum_core/pipeline/base.py +46 -0
  115. package/runtime/packages/core/src/video_sum_core/pipeline/real.py +3467 -0
  116. package/runtime/packages/core/src/video_sum_core/transcribe_subprocess.py +173 -0
  117. package/runtime/packages/core/src/video_sum_core/utils.py +127 -0
  118. package/runtime/packages/infra/pyproject.toml +16 -0
  119. package/runtime/packages/infra/src/video_sum_infra/__init__.py +1 -0
  120. package/runtime/packages/infra/src/video_sum_infra/app.py +39 -0
  121. package/runtime/packages/infra/src/video_sum_infra/config.py +393 -0
  122. package/runtime/packages/infra/src/video_sum_infra/db.py +34 -0
  123. package/runtime/packages/infra/src/video_sum_infra/logging.py +54 -0
  124. package/runtime/packages/infra/src/video_sum_infra/paths.py +6 -0
  125. package/runtime/packages/infra/src/video_sum_infra/runtime.py +485 -0
@@ -0,0 +1,393 @@
1
+ from pathlib import Path
2
+ from pydantic import Field, field_validator
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+ from video_sum_infra.runtime import (
6
+ default_cache_dir,
7
+ default_data_dir,
8
+ default_database_url,
9
+ default_host,
10
+ default_tasks_dir,
11
+ )
12
+
13
+ PREVIOUS_DEFAULT_SUMMARY_SYSTEM_PROMPT = (
14
+ "你是一名严谨的中文视频摘要助手。"
15
+ "你的唯一任务是基于用户提供的转写和分段信息,生成可直接展示给前端页面的结构化摘要。"
16
+ "不得编造视频中没有出现的信息,不得输出 JSON 以外的任何文字。"
17
+ "You must return valid json only."
18
+ )
19
+
20
+ PREVIOUS_DEFAULT_SUMMARY_USER_PROMPT_TEMPLATE = """请阅读下面的视频资料,并输出一个 JSON 对象。
21
+ 注意:你必须返回合法的 json 对象,且只返回 json。
22
+
23
+ 目标:生成一个可读性强、信息密度高、适合中文用户阅读的视频摘要。
24
+
25
+ 强约束:
26
+ 1. 必须输出合法 JSON,对象顶层只允许包含 title、overview、bulletPoints、chapters 四个字段。
27
+ 2. overview 必须是 2 到 4 句中文,概括视频核心观点、讨论主题和最终结论。
28
+ 3. bulletPoints 必须是 4 到 6 条中文要点,每条 18 到 60 个字,禁止空字符串,禁止重复改写同一条意思。
29
+ 4. chapters 必须是 3 到 6 个章节,每个章节必须包含 title、start、summary。
30
+ 5. chapter.title 要短,像小标题;chapter.summary 要概括该时间段内容,20 到 80 个字。
31
+ 6. start 必须使用视频里真实出现的时间点,单位为秒,按升序排列。
32
+ 7. 如果原文信息有限,也必须尽量提炼出非空 bulletPoints 和 chapters,不能返回空数组。
33
+ 8. 不要写“视频主要讲了”“本视频介绍了”这种空话,直接写内容。
34
+ 9. 不要引用不存在的数据,不要补充外部背景,不要分析说话者身份之外的隐含动机。
35
+
36
+ 写作要求:
37
+ - 保持中文自然、紧凑、具体。
38
+ - 优先提炼观点、结论、争议点、使用体验、推荐条件。
39
+ - chapters 应体现内容推进,而不是机械平均切分。
40
+
41
+ 输出格式示例:
42
+ {{"title":"","overview":"","bulletPoints":["", "", "", ""],"chapters":[{{"title":"","start":0,"summary":""}}]}}
43
+
44
+ 视频标题:{title}
45
+
46
+ 转写节选:
47
+ {transcript}
48
+
49
+ 分段数据节选:
50
+ {segments_json}"""
51
+
52
+ DEFAULT_SUMMARY_SYSTEM_PROMPT = (
53
+ "你是一名严谨、克制、信息密度优先的中文视频内容编辑。"
54
+ "你的任务不是泛泛总结,而是基于转写和分段信息,产出可以直接用于“知识卡片”页面的结构化内容。"
55
+ "所有内容都必须忠实原文,不得编造,不得补充外部资料,不得输出 JSON 以外的任何文字。"
56
+ "You must return valid json only."
57
+ )
58
+
59
+ DEFAULT_SUMMARY_USER_PROMPT_TEMPLATE = """请阅读下面的视频资料,并输出一个 JSON 对象。
60
+ 注意:你必须返回合法的 json 对象,且只返回 json。
61
+
62
+ 目标:
63
+ 生成一个适合详情页展示的结构化摘要,让用户在不看完整视频的情况下,也能快速理解:
64
+ 1. 这支视频核心在讲什么;
65
+ 2. 有哪些关键观点、论据、案例、争议和结论;
66
+ 3. 内容是如何逐步展开的。
67
+
68
+ 强约束:
69
+ 1. 必须输出合法 JSON,对象顶层只允许包含 title、overview、bulletPoints、chapters、chapterGroups 五个字段。
70
+ 2. title 必须是简洁、准确的中文标题,避免口号式空话。
71
+ 3. overview 必须写成 3 到 5 句中文,整体形成一段完整概述:
72
+ - 第 1 句交代主题或讨论对象;
73
+ - 中间句交代关键论点、论据、背景、冲突或方法;
74
+ - 最后 1 句交代结论、判断、影响或最终落点;
75
+ - 总体要具体、完整,适合单独作为“核心概览”展示。
76
+ 4. bulletPoints 必须是 5 到 8 条中文要点,每条 28 到 88 个字:
77
+ - 每条都要能单独成为一张知识卡片;
78
+ - 优先提炼事实、观点、因果、对比、条件、风险、建议、争议;
79
+ - 不要把同一件事拆成多条近义重复表达;
80
+ - 不要写“作者认为”“视频提到”这类低信息密度前缀,直接写结论。
81
+ 5. chapters 必须按内容自然分布生成,每个章节必须包含 title、start、summary:
82
+ - chapter.title 要像小标题,短而具体,能体现这一段的主题推进;
83
+ - chapter.summary 必须比普通概述更详细,写成 2 到 3 个短句或 40 到 120 个字,说明这一段具体讲了什么、举了什么例子、得出了什么判断;
84
+ - chapters 应体现内容推进关系,而不是机械平均切分;
85
+ - 章节数量不要预设固定值,应根据内容转折、主题切换、论证层次和视频时长自适应决定;
86
+ - 短视频可以较少章节,长视频或知识密度高的视频应适当增加章节,必要时可达到 9 到 12 个;
87
+ - 只有在确实进入新主题、新问题、新案例或新结论时才切出新章节,不要为了凑数量硬拆。
88
+ 6. chapterGroups 用来表示“大章节 / 小章节”层级,按真实结构归纳大章节;每个大章节必须包含 title、start、summary、children。
89
+ - chapterGroups.title 必须是有内容的主题名,禁止使用“大章节1”“第一部分”“Part 1”“Section 1”这类占位标题;
90
+ - 当原文层级明显时,大章节数量也应随内容自适应,不要固定成 2 到 4 组;
91
+ - 如果层级不明显,可以少量归并;如果层级明显,可以返回更多组,但不要机械平均分配。
92
+ 7. start 必须使用视频里真实出现的时间点,单位为秒,按升序排列。
93
+ 8. 如果原文信息有限,也必须尽量提炼出非空 bulletPoints 和 chapters,不能返回空数组。
94
+ 9. 不要写“视频主要讲了”“本视频介绍了”“作者首先”这类模板化空话,直接进入信息本体。
95
+ 10. 不要引用不存在的数据,不要补充外部背景,不要猜测说话者未明确表达的动机。
96
+
97
+ 写作要求:
98
+ - 保持中文自然、清楚、具体,避免官话和营销口吻。
99
+ - 优先保留高价值信息:定义、判断、证据、例子、条件、限制、影响、结论。
100
+ - 如果视频包含多个层次,overview 负责总览,bulletPoints 负责拆出关键结论,chapters 负责还原内容推进,chapterGroups 负责归纳章节层级。
101
+ - 标题必须像真实目录项,而不是编号占位符;宁可少而准,也不要为了数量固定而硬拆。
102
+
103
+ 输出格式示例:
104
+ {{"title":"","overview":"","bulletPoints":["", "", "", "", ""],"chapters":[{{"title":"","start":0,"summary":""}}],"chapterGroups":[{{"title":"","start":0,"summary":"","children":[{{"title":"","start":0,"summary":""}}]}}]}}
105
+
106
+ 视频标题:{title}
107
+
108
+ 转写节选:
109
+ {transcript}
110
+
111
+ 分段数据节选:
112
+ {segments_json}"""
113
+
114
+ DEFAULT_MINDMAP_SYSTEM_PROMPT = (
115
+ "你是一名擅长把学习内容重新组织为知识导图的中文内容编辑。"
116
+ "你的任务是基于已有结构化摘要和知识笔记,输出一个适合思维导图展示、信息密度充足、覆盖完整的 JSON 树。"
117
+ "所有内容都必须忠实原文,不得编造,不得补充外部资料,不得输出 JSON 以外的任何文字。"
118
+ "You must return valid json only."
119
+ )
120
+
121
+ DEFAULT_MINDMAP_USER_PROMPT_TEMPLATE = """请阅读下面的视频资料,并输出一个 JSON 对象。
122
+ 注意:你必须返回合法的 json 对象,且只返回 json。
123
+
124
+ 目标:
125
+ 把当前视频内容组织成一棵真正“像思维导图”的知识树。它必须以概念、主题、方法、结论之间的关系为核心,而不是把章节标题换个层级重新排列。最末层节点仍然必须能回到原视频片段。
126
+
127
+ 强约束:
128
+ 1. 顶层只允许包含 title、root、nodes 三个字段。
129
+ 2. root 必须是整棵导图的根节点 id。
130
+ 3. nodes 必须是数组,其中包含唯一的根节点;每个节点必须包含:
131
+ - id
132
+ - label
133
+ - type(只能是 root、theme、topic、leaf 之一)
134
+ - summary
135
+ - children
136
+ - time_anchor(仅 leaf 必填,其余可为空)
137
+ - source_chapter_titles
138
+ - source_chapter_starts
139
+ 4. 整体结构必须是树,不要输出交叉引用;最大深度为 root -> theme -> topic/leaf -> leaf。
140
+ 5. 顶层 theme 数量应为 4 到 8 个,每个 theme 下应有 3 到 6 个 topic 或 leaf;除非原内容本身很短,否则不要生成过于稀疏的导图。
141
+ 6. leaf 节点必须能映射到原章节,并带真实时间点;time_anchor 必须取自 source_chapter_starts 中最早的时间点。
142
+ 7. source_chapter_titles 和 source_chapter_starts 只保留最相关的 1 到 3 项,且数量一致。
143
+ 8. label 必须是有内容的主题名,禁止“主题1”“Part 1”“Section 1”等占位标题。
144
+ 9. summary 要适合学习复盘,直接写信息本体,不要重复整段知识笔记;theme/topic 的 summary 尽量写成 2 到 4 句,leaf 的 summary 至少要交代“结论 / 方法 / 条件 / 例子”中的两项。
145
+ 10. label 和 summary 内如果出现数学内容,优先使用 KaTeX 兼容的 LaTeX 写法,例如 `$\\frac{1}{n}$`、`$(-1)^n$`、`$\\varepsilon$-$N$`;不要输出无法解析的伪公式。
146
+ 11. 只允许输出 JSON;但 JSON 字符串内部允许包含少量 Markdown 和 `$...$` / `$$...$$` 数学公式。
147
+ 12. 不要输出空 children 字段以外的多余字段,不要输出解释说明。
148
+ 13. 不要把 `chapters` 或 `chapterGroups` 直接一一平移成 theme/topic;必须先做语义归纳,再组织层级。
149
+ 14. 如果多个章节都在讲同一个概念、同一种方法、同一类例子,应该聚合成一个主题,而不是拆成多个并列节点。
150
+ 15. 导图的每一层都应体现“父主题如何拆成子主题”,而不是简单的时间顺序。
151
+
152
+ 写作要求:
153
+ - 优先按“概念定义 / 推导方法 / 典型例子 / 易错点 / 结论判断 / 应用条件”这类知识结构重组。
154
+ - 根节点应该是整支视频真正的学习主题,不要只是视频标题原样重复,除非标题本身已经是明确概念。
155
+ - theme 层应该是 4 到 8 个最核心的大主题,彼此之间要有明显区分。
156
+ - topic 层应承担细化作用,只有当某个 theme 下确实存在两到三类不同子议题时才保留 topic;否则可直接挂 leaf。
157
+ - leaf 节点要具体、短促、可点击后立刻看懂,不要写成长句,也不要只是“第X部分”。
158
+ - 允许把多个来源章节压缩成一个更抽象、更像脑图节点的表达。
159
+ - 如果原文本身是教程或知识讲解,优先提炼“知识结构”;如果原文是评论或资讯,优先提炼“观点结构”和“因果关系”。
160
+ - 若视频包含公式、定义、判别条件、证明步骤、典型例题,不要省略它们;应把它们拆成独立主题或叶子节点,而不是只保留一个笼统标题。
161
+ - 不要怕信息多,只要层级清楚即可;优先保证“覆盖完整”和“节点可学”,不要只给每个主题一个空泛标签。
162
+ - 对于数学/理工类内容,优先把“定义、命题、判别条件、证明思路、典型例题、易错点”拆成不同节点;不要把整段证明压成一句泛泛描述。
163
+ - 如果一个 theme 下只生成了 1 个叶子节点,优先继续细化,除非原文确实只讲了这一点。
164
+ - 如果知识笔记已经给出分点、例题或条件,你应该把这些信息展开到对应节点,而不是只复述 theme 名称。
165
+ - 最终观感要像学习者自己整理出来的脑图,而不是讲稿目录。
166
+
167
+ 输出格式示例:
168
+ {{"title":"","root":"root","nodes":[{{"id":"root","label":"","type":"root","summary":"","children":[{{"id":"theme-1","label":"","type":"theme","summary":"","children":[{{"id":"leaf-1","label":"","type":"leaf","summary":"","children":[],"time_anchor":0,"source_chapter_titles":[""],"source_chapter_starts":[0]}}],"source_chapter_titles":[],"source_chapter_starts":[]}}],"source_chapter_titles":[],"source_chapter_starts":[]}}]}}
169
+
170
+ 视频标题:
171
+ {title}
172
+
173
+ 已有结构化摘要:
174
+ {summary_json}
175
+
176
+ 知识笔记:
177
+ {knowledge_note_markdown}
178
+ """
179
+
180
+ LEGACY_SUMMARY_SYSTEM_PROMPT = (
181
+ "你是一名中文视频总结助手。请基于转写内容输出 JSON,包含 title、overview、bulletPoints、chapters。内容必须忠实原文,不要编造。"
182
+ )
183
+
184
+ LEGACY_SUMMARY_USER_PROMPT_TEMPLATE = (
185
+ "请总结下面的视频转写。\n\n转写全文:\n{transcript}\n\n分段(JSON):\n{segments_json}\n\n"
186
+ "要求:\n1. bulletPoints 返回数组\n2. chapters 返回数组,每项包含 title、start、summary\n3. 输出必须是 JSON"
187
+ )
188
+
189
+ DEVICE_PREFERENCE_ALIASES = {
190
+ "auto": "auto",
191
+ "automatic": "auto",
192
+ "default": "auto",
193
+ "cpu": "cpu",
194
+ "cuda": "cuda",
195
+ "gpu": "cuda",
196
+ }
197
+
198
+ TRANSCRIPTION_PROVIDER_ALIASES = {
199
+ "local": "local",
200
+ "faster-whisper": "local",
201
+ "faster_whisper": "local",
202
+ "whisper": "local",
203
+ "siliconflow": "siliconflow",
204
+ "silicon-flow": "siliconflow",
205
+ "silicon_flow": "siliconflow",
206
+ }
207
+
208
+ KNOWLEDGE_LLM_MODE_ALIASES = {
209
+ "same_as_main": "same_as_main",
210
+ "same-as-main": "same_as_main",
211
+ "same": "same_as_main",
212
+ "custom": "custom",
213
+ "independent": "custom",
214
+ }
215
+
216
+
217
+ def normalize_device_preference(value: str | None, default: str = "cpu") -> str:
218
+ normalized = str(value or "").strip().lower()
219
+ if not normalized:
220
+ return default
221
+ return DEVICE_PREFERENCE_ALIASES.get(normalized, default)
222
+
223
+
224
+ def normalize_transcription_provider(value: str | None, default: str = "siliconflow") -> str:
225
+ normalized = str(value or "").strip().lower()
226
+ if not normalized:
227
+ return default
228
+ return TRANSCRIPTION_PROVIDER_ALIASES.get(normalized, default)
229
+
230
+
231
+ def normalize_knowledge_llm_mode(value: str | None, default: str = "same_as_main") -> str:
232
+ normalized = str(value or "").strip().lower()
233
+ if not normalized:
234
+ return default
235
+ return KNOWLEDGE_LLM_MODE_ALIASES.get(normalized, default)
236
+
237
+
238
+ def recommend_task_concurrency(settings: "ServiceSettings", *, cuda_available: bool | None = None) -> int:
239
+ provider = normalize_transcription_provider(settings.transcription_provider)
240
+ if provider == "local":
241
+ return 1
242
+ if cuda_available is True:
243
+ return 2
244
+
245
+ runtime_channel = str(settings.runtime_channel or "").strip().lower()
246
+ if runtime_channel.startswith("gpu-"):
247
+ return 2
248
+
249
+ device_preference = normalize_device_preference(settings.device_preference)
250
+ if device_preference == "cuda":
251
+ return 2
252
+
253
+ whisper_device = str(settings.whisper_device or "").strip().lower()
254
+ if whisper_device == "cuda":
255
+ return 2
256
+
257
+ if provider == "siliconflow":
258
+ return 2
259
+ return 1
260
+
261
+
262
+ def recommend_mindmap_concurrency() -> int:
263
+ return 1
264
+
265
+
266
+ def normalize_knowledge_index_auto_rebuild(value: str | None) -> str:
267
+ normalized = str(value or "disabled").strip().lower()
268
+ if normalized in {"on_task_completed", "task_completed", "completed"}:
269
+ return "on_task_completed"
270
+ return "disabled"
271
+
272
+
273
+ class ServiceSettings(BaseSettings):
274
+ host: str = Field(default_factory=default_host)
275
+ port: int = 3838
276
+ data_dir: Path = Field(default_factory=default_data_dir)
277
+ cache_dir: Path = Field(default_factory=default_cache_dir)
278
+ tasks_dir: Path = Field(default_factory=default_tasks_dir)
279
+ database_url: str = Field(default_factory=default_database_url)
280
+ whisper_model: str = "tiny"
281
+ whisper_device: str = "cpu"
282
+ whisper_compute_type: str = "int8"
283
+ transcription_provider: str = "siliconflow"
284
+ device_preference: str = "cpu"
285
+ compute_type: str = "int8"
286
+ model_mode: str = "fixed"
287
+ fixed_model: str = "tiny"
288
+ siliconflow_asr_base_url: str = "https://api.siliconflow.cn/v1"
289
+ siliconflow_asr_model: str = "TeleAI/TeleSpeechASR"
290
+ siliconflow_asr_api_key: str = ""
291
+ cuda_variant: str = "cu128"
292
+ runtime_channel: str = "base"
293
+ output_dir: str = ""
294
+ preserve_temp_audio: bool = False
295
+ enable_cache: bool = True
296
+ language: str = "zh"
297
+ summary_mode: str = "llm"
298
+ llm_enabled: bool = False
299
+ auto_generate_mindmap: bool = False
300
+ llm_provider: str = "openai-compatible"
301
+ llm_api_key: str = ""
302
+ llm_base_url: str = ""
303
+ llm_model: str = ""
304
+ knowledge_llm_mode: str = "same_as_main"
305
+ knowledge_llm_enabled: bool = False
306
+ knowledge_llm_api_key: str = ""
307
+ knowledge_llm_base_url: str = ""
308
+ knowledge_llm_model: str = ""
309
+ knowledge_enabled: bool = False
310
+ knowledge_index_auto_rebuild: str = "disabled"
311
+ summary_system_prompt: str = DEFAULT_SUMMARY_SYSTEM_PROMPT
312
+ summary_user_prompt_template: str = DEFAULT_SUMMARY_USER_PROMPT_TEMPLATE
313
+ mindmap_system_prompt: str = DEFAULT_MINDMAP_SYSTEM_PROMPT
314
+ mindmap_user_prompt_template: str = DEFAULT_MINDMAP_USER_PROMPT_TEMPLATE
315
+ summary_chunk_target_chars: int = 2200
316
+ summary_chunk_overlap_segments: int = 2
317
+ task_concurrency: int = 2
318
+ mindmap_concurrency: int = 1
319
+ summary_chunk_concurrency: int = 2
320
+ summary_chunk_retry_count: int = 2
321
+ ytdlp_cookies_file: str = ""
322
+ ytdlp_cookies_browser: str = ""
323
+
324
+ model_config = SettingsConfigDict(
325
+ env_prefix="VIDEO_SUM_",
326
+ env_file=".env",
327
+ extra="ignore",
328
+ )
329
+
330
+ @field_validator("device_preference", mode="before")
331
+ @classmethod
332
+ def _normalize_device_preference(cls, value: str | None) -> str:
333
+ return normalize_device_preference(value)
334
+
335
+ @field_validator("transcription_provider", mode="before")
336
+ @classmethod
337
+ def _normalize_transcription_provider(cls, value: str | None) -> str:
338
+ return normalize_transcription_provider(value)
339
+
340
+ @field_validator("knowledge_llm_mode", mode="before")
341
+ @classmethod
342
+ def _normalize_knowledge_llm_mode(cls, value: str | None) -> str:
343
+ return normalize_knowledge_llm_mode(value)
344
+
345
+ @field_validator("knowledge_index_auto_rebuild", mode="before")
346
+ @classmethod
347
+ def _normalize_knowledge_index_auto_rebuild(cls, value: str | None) -> str:
348
+ return normalize_knowledge_index_auto_rebuild(value)
349
+
350
+ @field_validator("summary_chunk_overlap_segments", "task_concurrency", "mindmap_concurrency", "summary_chunk_concurrency", "summary_chunk_retry_count", mode="before")
351
+ @classmethod
352
+ def _coerce_positive_int(cls, value: int | str | None) -> int:
353
+ parsed = int(value or 1)
354
+ return max(1, parsed)
355
+
356
+ def resolve_whisper_runtime(
357
+ self,
358
+ cuda_available: bool,
359
+ ) -> tuple[str, str, str]:
360
+ if self.model_mode == "auto":
361
+ model = "large-v3-turbo" if cuda_available else "base"
362
+ else:
363
+ model = self.fixed_model or self.whisper_model or "tiny"
364
+
365
+ device_preference = normalize_device_preference(self.device_preference)
366
+
367
+ if device_preference == "auto":
368
+ device = "cuda" if cuda_available else "cpu"
369
+ elif device_preference == "cuda" and cuda_available:
370
+ device = "cuda"
371
+ elif device_preference == "cuda" and not cuda_available:
372
+ device = "cpu"
373
+ else:
374
+ device = "cpu"
375
+
376
+ if self.compute_type == "auto":
377
+ compute_type = "float16" if device == "cuda" else "int8"
378
+ elif device == "cuda" and self.compute_type == "int8":
379
+ compute_type = "int8_float16"
380
+ else:
381
+ compute_type = self.compute_type or self.whisper_compute_type or "int8"
382
+
383
+ return model, device, compute_type
384
+
385
+ def with_resolved_runtime(self, cuda_available: bool) -> "ServiceSettings":
386
+ model, device, compute_type = self.resolve_whisper_runtime(cuda_available=cuda_available)
387
+ return self.model_copy(
388
+ update={
389
+ "whisper_model": model,
390
+ "whisper_device": device,
391
+ "whisper_compute_type": compute_type,
392
+ }
393
+ )
@@ -0,0 +1,34 @@
1
+ import sqlite3
2
+ from contextlib import contextmanager
3
+ from pathlib import Path
4
+ from urllib.parse import urlparse
5
+
6
+
7
+ def sqlite_path_from_url(database_url: str) -> Path:
8
+ if database_url.startswith("sqlite:///"):
9
+ return Path(database_url.removeprefix("sqlite:///"))
10
+ parsed = urlparse(database_url)
11
+ if parsed.scheme == "sqlite":
12
+ return Path(parsed.path.lstrip("/"))
13
+ raise ValueError(f"Unsupported database url: {database_url}")
14
+
15
+
16
+ def connect_sqlite(database_url: str) -> sqlite3.Connection:
17
+ path = sqlite_path_from_url(database_url)
18
+ path.parent.mkdir(parents=True, exist_ok=True)
19
+ connection = sqlite3.connect(path, check_same_thread=False)
20
+ connection.row_factory = sqlite3.Row
21
+ return connection
22
+
23
+
24
+ @contextmanager
25
+ def sqlite_cursor(connection: sqlite3.Connection):
26
+ cursor = connection.cursor()
27
+ try:
28
+ yield cursor
29
+ connection.commit()
30
+ except Exception:
31
+ connection.rollback()
32
+ raise
33
+ finally:
34
+ cursor.close()
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import logging.config
5
+ from logging.handlers import RotatingFileHandler
6
+
7
+ from video_sum_infra.runtime import log_dir, service_log_path
8
+
9
+
10
+ LOG_FORMAT = "%(asctime)s %(levelname)s [pid=%(process)d tid=%(threadName)s] %(name)s | %(message)s"
11
+
12
+
13
+ def configure_logging() -> None:
14
+ log_dir().mkdir(parents=True, exist_ok=True)
15
+ logging.config.dictConfig(
16
+ {
17
+ "version": 1,
18
+ "disable_existing_loggers": False,
19
+ "formatters": {
20
+ "standard": {
21
+ "format": LOG_FORMAT,
22
+ }
23
+ },
24
+ "handlers": {
25
+ "console": {
26
+ "class": "logging.StreamHandler",
27
+ "formatter": "standard",
28
+ "level": "INFO",
29
+ },
30
+ "file": {
31
+ "class": "logging.handlers.RotatingFileHandler",
32
+ "formatter": "standard",
33
+ "level": "INFO",
34
+ "filename": str(service_log_path()),
35
+ "maxBytes": 2 * 1024 * 1024,
36
+ "backupCount": 3,
37
+ "encoding": "utf-8",
38
+ },
39
+ },
40
+ "root": {
41
+ "handlers": ["console", "file"],
42
+ "level": "INFO",
43
+ },
44
+ "loggers": {
45
+ "uvicorn.access": {"level": "WARNING", "propagate": False, "handlers": ["console", "file"]},
46
+ "httpx": {"level": "WARNING", "propagate": False, "handlers": ["console", "file"]},
47
+ "httpcore": {"level": "WARNING", "propagate": False, "handlers": ["console", "file"]},
48
+ "faster_whisper": {"level": "WARNING", "propagate": False, "handlers": ["console", "file"]},
49
+ "asyncio": {"level": "WARNING", "propagate": True},
50
+ "video_sum_service": {"level": "INFO", "propagate": True},
51
+ "video_sum_core": {"level": "INFO", "propagate": True},
52
+ },
53
+ }
54
+ )
@@ -0,0 +1,6 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def ensure_directory(path: Path) -> Path:
5
+ path.mkdir(parents=True, exist_ok=True)
6
+ return path