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,242 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from itertools import combinations
5
+
6
+ from fastapi import HTTPException
7
+
8
+ from video_sum_infra.config import ServiceSettings
9
+ from video_sum_service.knowledge.local_llm import chat_knowledge_llm, parse_json_payload
10
+ from video_sum_service.repository import SqliteTaskRepository
11
+ from video_sum_service.schemas import (
12
+ KnowledgeAutoTagResponse,
13
+ KnowledgeAutoTagVideoResult,
14
+ KnowledgeNetworkLink,
15
+ KnowledgeNetworkNode,
16
+ KnowledgeNetworkResponse,
17
+ TagItem,
18
+ VideoTagRecord,
19
+ )
20
+
21
+
22
+ class TagService:
23
+ def __init__(self, repository: SqliteTaskRepository, settings: ServiceSettings) -> None:
24
+ self._repository = repository
25
+ self._settings = settings
26
+
27
+ def add_tag(self, video_id: str, tag: str, source: str = "manual", confidence: float = 1.0) -> bool:
28
+ return self._repository.add_video_tag(video_id, tag, source=source, confidence=confidence)
29
+
30
+ def remove_tag(self, video_id: str, tag: str) -> bool:
31
+ return self._repository.remove_video_tag(video_id, tag)
32
+
33
+ def get_tags_for_video(self, video_id: str) -> list[VideoTagRecord]:
34
+ return self._repository.list_video_tags(video_id)
35
+
36
+ def get_all_tags(self) -> list[TagItem]:
37
+ return [TagItem.model_validate(item) for item in self._repository.list_all_tags()]
38
+
39
+ def auto_tag_video(self, video_id: str, llm_model: str | None = None) -> list[str]:
40
+ del llm_model
41
+ video = self._repository.get_video_asset(video_id)
42
+ if video is None or video.latest_result is None:
43
+ raise HTTPException(status_code=404, detail="Video not found or has no summary result.")
44
+
45
+ result = video.latest_result
46
+ content = "\n\n".join(
47
+ [
48
+ f"标题:{video.title}",
49
+ f"概览:{result.overview}",
50
+ "知识笔记:",
51
+ result.knowledge_note_markdown or "",
52
+ "章节:",
53
+ "\n".join(
54
+ f"- {chapter.get('title')}: {chapter.get('summary')}"
55
+ for chapter in result.timeline
56
+ if isinstance(chapter, dict)
57
+ ),
58
+ ]
59
+ ).strip()
60
+
61
+ response_text, _body = chat_knowledge_llm(
62
+ self._settings,
63
+ system_prompt=(
64
+ "你是一个知识库标签助手。请根据视频摘要和知识笔记,为视频生成 3 到 8 个简洁中文标签。"
65
+ "只返回合法 JSON,对象格式必须为 {\"tags\": [\"标签1\", \"标签2\"]}。"
66
+ ),
67
+ user_prompt=content,
68
+ require_json=True,
69
+ max_tokens=200,
70
+ temperature=0.1,
71
+ )
72
+ payload = parse_json_payload(response_text)
73
+ raw_tags = payload.get("tags")
74
+ if not isinstance(raw_tags, list):
75
+ raise HTTPException(status_code=502, detail="知识库 LLM 返回的标签格式不正确。")
76
+
77
+ normalized_tags: list[str] = []
78
+ seen: set[str] = set()
79
+ for item in raw_tags:
80
+ tag = str(item or "").strip()
81
+ if not tag or tag in seen:
82
+ continue
83
+ seen.add(tag)
84
+ normalized_tags.append(tag)
85
+ self._repository.add_video_tag(video_id, tag, source="auto_llm", confidence=0.8)
86
+ return normalized_tags
87
+
88
+ def batch_auto_tag(self, video_ids: list[str] | None = None) -> KnowledgeAutoTagResponse:
89
+ target_video_ids = video_ids or self._repository.list_untagged_video_ids()
90
+ items: list[KnowledgeAutoTagVideoResult] = []
91
+ for video_id in target_video_ids:
92
+ tags = self.auto_tag_video(video_id)
93
+ items.append(KnowledgeAutoTagVideoResult(video_id=video_id, tags=tags))
94
+ return KnowledgeAutoTagResponse(items=items)
95
+
96
+ def get_network_data(
97
+ self,
98
+ selected_tags: list[str] | None = None,
99
+ *,
100
+ max_tags: int = 12,
101
+ max_videos: int = 8,
102
+ ) -> KnowledgeNetworkResponse:
103
+ all_tags = self._repository.list_all_tags()
104
+ all_video_tags = self._repository.list_all_video_tags()
105
+ selected = [tag for tag in dict.fromkeys(selected_tags or []) if tag]
106
+ tag_counts = {str(item["tag"]): int(item["count"]) for item in all_tags}
107
+ video_tag_map: dict[str, list[str]] = {}
108
+ for item in all_video_tags:
109
+ video_tag_map.setdefault(item.video_id, []).append(item.tag)
110
+
111
+ tag_video_map: dict[str, set[str]] = {}
112
+ cooccurrence: Counter[tuple[str, str]] = Counter()
113
+ for video_id, tags in video_tag_map.items():
114
+ unique_tags = sorted({tag for tag in tags if tag})
115
+ for tag in unique_tags:
116
+ tag_video_map.setdefault(tag, set()).add(video_id)
117
+ for left, right in combinations(unique_tags, 2):
118
+ cooccurrence[(left, right)] += 1
119
+
120
+ tag_degree: Counter[str] = Counter()
121
+ for (left, right), weight in cooccurrence.items():
122
+ tag_degree[left] += weight
123
+ tag_degree[right] += weight
124
+
125
+ def tag_rank(tag: str) -> tuple[int, int, str]:
126
+ return (tag_degree.get(tag, 0), tag_counts.get(tag, 0), tag)
127
+
128
+ if selected:
129
+ existing_selected = [tag for tag in selected if tag in tag_counts]
130
+ related_scores: Counter[str] = Counter()
131
+ for current_tag in existing_selected:
132
+ for (left, right), weight in cooccurrence.items():
133
+ if left == current_tag and right not in existing_selected:
134
+ related_scores[right] += weight
135
+ elif right == current_tag and left not in existing_selected:
136
+ related_scores[left] += weight
137
+ related_tags = [
138
+ tag
139
+ for tag, _score in sorted(
140
+ related_scores.items(),
141
+ key=lambda item: (item[1], tag_degree.get(item[0], 0), tag_counts.get(item[0], 0), item[0]),
142
+ reverse=True,
143
+ )
144
+ ]
145
+ visible_tags = existing_selected + [
146
+ tag for tag in related_tags if tag not in existing_selected
147
+ ][: max(0, max_tags - len(existing_selected))]
148
+ candidate_videos = []
149
+ for video_id, tags in video_tag_map.items():
150
+ overlap = len(set(tags) & set(existing_selected))
151
+ if overlap <= 0:
152
+ continue
153
+ candidate_videos.append((video_id, overlap, len(tags)))
154
+ candidate_videos.sort(key=lambda item: (item[1], item[2], item[0]), reverse=True)
155
+ visible_video_ids = [video_id for video_id, _overlap, _tag_total in candidate_videos[:max_videos]]
156
+ mode = "focus"
157
+ else:
158
+ ranked_tags = sorted(tag_counts, key=tag_rank, reverse=True)
159
+ visible_tags = ranked_tags[:max_tags]
160
+ visible_video_ids: list[str] = []
161
+ mode = "overview"
162
+
163
+ visible_tag_set = set(visible_tags)
164
+ nodes: list[KnowledgeNetworkNode] = []
165
+ for tag in visible_tags:
166
+ nodes.append(
167
+ KnowledgeNetworkNode(
168
+ id=f"tag_{tag}",
169
+ label=tag,
170
+ type="tag",
171
+ count=tag_counts.get(tag, 0),
172
+ degree=tag_degree.get(tag, 0),
173
+ focus=tag in selected,
174
+ video_count=len(tag_video_map.get(tag, set())),
175
+ )
176
+ )
177
+
178
+ links: list[KnowledgeNetworkLink] = []
179
+ if selected:
180
+ for video_id in visible_video_ids:
181
+ video = self._repository.get_video_asset(video_id)
182
+ if video is None:
183
+ continue
184
+ video_tags = sorted(tag for tag in set(video_tag_map.get(video_id, [])) if tag in visible_tag_set)
185
+ if not video_tags:
186
+ continue
187
+ nodes.append(
188
+ KnowledgeNetworkNode(
189
+ id=f"video_{video.video_id}",
190
+ label=video.title,
191
+ type="video",
192
+ tags=video_tags,
193
+ video_count=1,
194
+ )
195
+ )
196
+ for tag in video_tags[:3]:
197
+ links.append(
198
+ KnowledgeNetworkLink(
199
+ source=f"tag_{tag}",
200
+ target=f"video_{video.video_id}",
201
+ weight=1.0,
202
+ kind="association",
203
+ )
204
+ )
205
+ for (left, right), weight in cooccurrence.most_common():
206
+ if left in visible_tag_set and right in visible_tag_set:
207
+ links.append(
208
+ KnowledgeNetworkLink(
209
+ source=f"tag_{left}",
210
+ target=f"tag_{right}",
211
+ weight=float(weight),
212
+ kind="cooccurrence",
213
+ )
214
+ )
215
+ if len([item for item in links if item.kind == "cooccurrence"]) >= max(8, len(visible_tags) * 2):
216
+ break
217
+ else:
218
+ fallback_links: list[KnowledgeNetworkLink] = []
219
+ for (left, right), weight in cooccurrence.most_common():
220
+ if left in visible_tag_set and right in visible_tag_set:
221
+ candidate = KnowledgeNetworkLink(
222
+ source=f"tag_{left}",
223
+ target=f"tag_{right}",
224
+ weight=float(weight),
225
+ kind="cooccurrence",
226
+ )
227
+ if weight >= 2:
228
+ links.append(candidate)
229
+ else:
230
+ fallback_links.append(candidate)
231
+ if len(links) >= max(10, len(visible_tags) * 2):
232
+ break
233
+ if not links:
234
+ links.extend(fallback_links[: max(1, min(6, len(fallback_links)))])
235
+
236
+ return KnowledgeNetworkResponse(
237
+ nodes=nodes,
238
+ links=links,
239
+ mode=mode,
240
+ hidden_tag_count=max(0, len(tag_counts) - len(visible_tags)),
241
+ selected_tags=selected,
242
+ )
@@ -0,0 +1,31 @@
1
+ import logging
2
+
3
+ from uvicorn import run as uvicorn_run
4
+
5
+ from video_sum_infra.logging import configure_logging
6
+ from video_sum_infra.paths import ensure_directory
7
+ from video_sum_infra.runtime import activate_runtime_pythonpath, bootstrap_managed_runtime, prepend_runtime_path
8
+ from video_sum_service.app import app, settings_manager
9
+
10
+ logger = logging.getLogger("video_sum_service.main")
11
+
12
+
13
+ def run() -> None:
14
+ settings = settings_manager.current
15
+ bootstrap_managed_runtime(settings.runtime_channel)
16
+ prepend_runtime_path(settings.runtime_channel)
17
+ activate_runtime_pythonpath(settings.runtime_channel)
18
+ configure_logging()
19
+ logger.info(
20
+ "starting service host=%s port=%s data_dir=%s cache_dir=%s tasks_dir=%s runtime_channel=%s",
21
+ settings.host,
22
+ settings.port,
23
+ settings.data_dir,
24
+ settings.cache_dir,
25
+ settings.tasks_dir,
26
+ settings.runtime_channel,
27
+ )
28
+ ensure_directory(settings.data_dir)
29
+ ensure_directory(settings.cache_dir)
30
+ ensure_directory(settings.tasks_dir)
31
+ uvicorn_run(app, host=settings.host, port=settings.port, access_log=False)