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.
- package/README.md +4 -4
- package/bin/bilisum.js +157 -76
- package/package.json +7 -2
- package/runtime/VERSION +1 -0
- package/runtime/apps/service/pyproject.toml +29 -0
- package/runtime/apps/service/src/video_sum_service/__init__.py +1 -0
- package/runtime/apps/service/src/video_sum_service/__main__.py +5 -0
- package/runtime/apps/service/src/video_sum_service/app.py +480 -0
- package/runtime/apps/service/src/video_sum_service/context.py +24 -0
- package/runtime/apps/service/src/video_sum_service/integrations.py +264 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/__init__.py +5 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/index_service.py +408 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/local_llm.py +319 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/rag_service.py +492 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/tag_service.py +242 -0
- package/runtime/apps/service/src/video_sum_service/main.py +31 -0
- package/runtime/apps/service/src/video_sum_service/repository.py +942 -0
- package/runtime/apps/service/src/video_sum_service/routers/__init__.py +1 -0
- package/runtime/apps/service/src/video_sum_service/routers/knowledge.py +272 -0
- package/runtime/apps/service/src/video_sum_service/routers/system.py +280 -0
- package/runtime/apps/service/src/video_sum_service/routers/tasks.py +287 -0
- package/runtime/apps/service/src/video_sum_service/routers/videos.py +766 -0
- package/runtime/apps/service/src/video_sum_service/runtime_support.py +1007 -0
- package/runtime/apps/service/src/video_sum_service/schemas.py +418 -0
- package/runtime/apps/service/src/video_sum_service/settings_manager.py +130 -0
- package/runtime/apps/service/src/video_sum_service/task_artifacts.py +102 -0
- package/runtime/apps/service/src/video_sum_service/task_exports.py +108 -0
- package/runtime/apps/service/src/video_sum_service/transcribe_worker.py +9 -0
- package/runtime/apps/service/src/video_sum_service/video_assets.py +623 -0
- package/runtime/apps/service/src/video_sum_service/worker.py +381 -0
- package/runtime/apps/web/static/apple-touch-icon.png +0 -0
- package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/runtime/apps/web/static/assets/icons/icon-180.png +0 -0
- package/runtime/apps/web/static/assets/icons/icon-512.png +0 -0
- package/runtime/apps/web/static/assets/icons/icon.svg +17 -0
- package/runtime/apps/web/static/assets/index-Cj5PVjQg.js +388 -0
- package/runtime/apps/web/static/assets/index-DU2t4_7s.css +1 -0
- package/runtime/apps/web/static/favicon-32x32.png +0 -0
- package/runtime/apps/web/static/favicon.ico +0 -0
- package/runtime/apps/web/static/favicon.svg +17 -0
- package/runtime/apps/web/static/index.html +19 -0
- package/runtime/apps/web/static/js/api.js +123 -0
- package/runtime/apps/web/static/js/main.js +932 -0
- package/runtime/apps/web/static/js/state.js +28 -0
- package/runtime/apps/web/static/js/utils.js +81 -0
- package/runtime/apps/web/static/js/views/home.js +604 -0
- package/runtime/apps/web/static/js/views/settings.js +477 -0
- package/runtime/apps/web/static/styles.css +2206 -0
- package/runtime/packages/core/pyproject.toml +18 -0
- package/runtime/packages/core/src/video_sum_core/__init__.py +1 -0
- package/runtime/packages/core/src/video_sum_core/errors.py +22 -0
- package/runtime/packages/core/src/video_sum_core/markdown_exports.py +154 -0
- package/runtime/packages/core/src/video_sum_core/models/__init__.py +1 -0
- package/runtime/packages/core/src/video_sum_core/models/tasks.py +70 -0
- package/runtime/packages/core/src/video_sum_core/pipeline/__init__.py +1 -0
- package/runtime/packages/core/src/video_sum_core/pipeline/base.py +46 -0
- package/runtime/packages/core/src/video_sum_core/pipeline/real.py +3467 -0
- package/runtime/packages/core/src/video_sum_core/transcribe_subprocess.py +173 -0
- package/runtime/packages/core/src/video_sum_core/utils.py +127 -0
- package/runtime/packages/infra/pyproject.toml +16 -0
- package/runtime/packages/infra/src/video_sum_infra/__init__.py +1 -0
- package/runtime/packages/infra/src/video_sum_infra/app.py +39 -0
- package/runtime/packages/infra/src/video_sum_infra/config.py +393 -0
- package/runtime/packages/infra/src/video_sum_infra/db.py +34 -0
- package/runtime/packages/infra/src/video_sum_infra/logging.py +54 -0
- package/runtime/packages/infra/src/video_sum_infra/paths.py +6 -0
- package/runtime/packages/infra/src/video_sum_infra/runtime.py +485 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from video_sum_core.models.tasks import MindMapNode, TaskInput, TaskMindMap, TaskResult, TaskStatus
|
|
8
|
+
from video_sum_core.utils import extract_bilibili_page
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskCreateRequest(TaskInput):
|
|
12
|
+
video_id: str | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResummaryRequest(BaseModel):
|
|
16
|
+
task_id: str | None = None
|
|
17
|
+
page_number: int | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VideoTaskCreateRequest(BaseModel):
|
|
21
|
+
page_number: int | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VideoTaskBatchRequest(BaseModel):
|
|
25
|
+
page_numbers: list[int] = Field(default_factory=list)
|
|
26
|
+
confirm: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AggregateSummaryRequest(BaseModel):
|
|
30
|
+
page_numbers: list[int] | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TaskMarkdownExportRequest(BaseModel):
|
|
34
|
+
target: Literal["markdown", "obsidian"] = "obsidian"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class VideoTaskBatchPageResponse(BaseModel):
|
|
38
|
+
page_number: int
|
|
39
|
+
page_title: str | None = None
|
|
40
|
+
action: str
|
|
41
|
+
reason: str | None = None
|
|
42
|
+
existing_task_id: str | None = None
|
|
43
|
+
existing_status: TaskStatus | None = None
|
|
44
|
+
has_existing_result: bool = False
|
|
45
|
+
task: "TaskDetailResponse | None" = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class VideoTaskBatchResponse(BaseModel):
|
|
49
|
+
operation: str
|
|
50
|
+
requested_page_numbers: list[int] = Field(default_factory=list)
|
|
51
|
+
requires_confirmation: bool = False
|
|
52
|
+
created_tasks: list["TaskDetailResponse"] = Field(default_factory=list)
|
|
53
|
+
skipped_pages: list[VideoTaskBatchPageResponse] = Field(default_factory=list)
|
|
54
|
+
conflict_pages: list[VideoTaskBatchPageResponse] = Field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class VideoProbeRequest(BaseModel):
|
|
58
|
+
url: str
|
|
59
|
+
force_refresh: bool = False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class VideoPageOptionResponse(BaseModel):
|
|
63
|
+
page: int
|
|
64
|
+
title: str
|
|
65
|
+
source_url: str
|
|
66
|
+
cover_url: str = ""
|
|
67
|
+
duration: float | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class VideoAssetSummaryResponse(BaseModel):
|
|
71
|
+
video_id: str
|
|
72
|
+
canonical_id: str
|
|
73
|
+
platform: str
|
|
74
|
+
title: str
|
|
75
|
+
source_url: str
|
|
76
|
+
cover_url: str = ""
|
|
77
|
+
duration: float | None = None
|
|
78
|
+
latest_task_id: str | None = None
|
|
79
|
+
latest_status: TaskStatus | None = None
|
|
80
|
+
latest_stage: str | None = None
|
|
81
|
+
has_result: bool = False
|
|
82
|
+
is_favorite: bool = False
|
|
83
|
+
favorite_updated_at: datetime | None = None
|
|
84
|
+
pages: list[VideoPageOptionResponse] = Field(default_factory=list)
|
|
85
|
+
created_at: datetime
|
|
86
|
+
updated_at: datetime
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class VideoAssetDetailResponse(VideoAssetSummaryResponse):
|
|
90
|
+
latest_result: TaskResult | None = None
|
|
91
|
+
latest_error_message: str | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class VideoAssetRecord(BaseModel):
|
|
95
|
+
video_id: str = Field(default_factory=lambda: uuid4().hex)
|
|
96
|
+
canonical_id: str
|
|
97
|
+
platform: str = "bilibili"
|
|
98
|
+
title: str
|
|
99
|
+
source_url: str
|
|
100
|
+
cover_url: str = ""
|
|
101
|
+
duration: float | None = None
|
|
102
|
+
latest_task_id: str | None = None
|
|
103
|
+
latest_status: TaskStatus | None = None
|
|
104
|
+
latest_stage: str | None = None
|
|
105
|
+
latest_result: TaskResult | None = None
|
|
106
|
+
latest_error_message: str | None = None
|
|
107
|
+
is_favorite: bool = False
|
|
108
|
+
favorite_updated_at: datetime | None = None
|
|
109
|
+
pages: list[VideoPageOptionResponse] = Field(default_factory=list)
|
|
110
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
111
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
112
|
+
|
|
113
|
+
def to_summary(self) -> VideoAssetSummaryResponse:
|
|
114
|
+
return VideoAssetSummaryResponse(
|
|
115
|
+
video_id=self.video_id,
|
|
116
|
+
canonical_id=self.canonical_id,
|
|
117
|
+
platform=self.platform,
|
|
118
|
+
title=self.title,
|
|
119
|
+
source_url=self.source_url,
|
|
120
|
+
cover_url=self.cover_url,
|
|
121
|
+
duration=self.duration,
|
|
122
|
+
latest_task_id=self.latest_task_id,
|
|
123
|
+
latest_status=self.latest_status,
|
|
124
|
+
latest_stage=self.latest_stage,
|
|
125
|
+
has_result=self.latest_result is not None,
|
|
126
|
+
is_favorite=self.is_favorite,
|
|
127
|
+
favorite_updated_at=self.favorite_updated_at,
|
|
128
|
+
pages=self.pages,
|
|
129
|
+
created_at=self.created_at,
|
|
130
|
+
updated_at=self.updated_at,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def to_detail(self) -> VideoAssetDetailResponse:
|
|
134
|
+
return VideoAssetDetailResponse(
|
|
135
|
+
**self.to_summary().model_dump(),
|
|
136
|
+
latest_result=self.latest_result,
|
|
137
|
+
latest_error_message=self.latest_error_message,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class VideoProbeResponse(BaseModel):
|
|
142
|
+
video: VideoAssetSummaryResponse
|
|
143
|
+
cached: bool = False
|
|
144
|
+
requires_selection: bool = False
|
|
145
|
+
pages: list[VideoPageOptionResponse] = Field(default_factory=list)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TaskSummaryResponse(BaseModel):
|
|
149
|
+
task_id: str
|
|
150
|
+
video_id: str | None = None
|
|
151
|
+
status: TaskStatus
|
|
152
|
+
input_type: str
|
|
153
|
+
source: str
|
|
154
|
+
title: str | None = None
|
|
155
|
+
page_number: int | None = None
|
|
156
|
+
page_title: str | None = None
|
|
157
|
+
created_at: datetime
|
|
158
|
+
updated_at: datetime
|
|
159
|
+
llm_total_tokens: int | None = None
|
|
160
|
+
task_duration_seconds: float | None = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TaskDetailResponse(TaskSummaryResponse):
|
|
164
|
+
result: TaskResult | None = None
|
|
165
|
+
error_code: str | None = None
|
|
166
|
+
error_message: str | None = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TaskMindMapResponse(BaseModel):
|
|
170
|
+
task_id: str
|
|
171
|
+
status: str = "idle"
|
|
172
|
+
error_message: str | None = None
|
|
173
|
+
updated_at: datetime | None = None
|
|
174
|
+
mindmap: TaskMindMap | None = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TaskMarkdownExportResponse(BaseModel):
|
|
178
|
+
task_id: str
|
|
179
|
+
target_format: Literal["markdown", "obsidian"]
|
|
180
|
+
path: str
|
|
181
|
+
directory: str
|
|
182
|
+
file_name: str
|
|
183
|
+
overwritten: bool = False
|
|
184
|
+
artifact_key: str
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TaskEventResponse(BaseModel):
|
|
188
|
+
event_id: str
|
|
189
|
+
task_id: str
|
|
190
|
+
stage: str
|
|
191
|
+
progress: int
|
|
192
|
+
message: str
|
|
193
|
+
created_at: datetime
|
|
194
|
+
payload: dict[str, object] = Field(default_factory=dict)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class TaskProgressResponse(BaseModel):
|
|
198
|
+
task_id: str
|
|
199
|
+
status: TaskStatus
|
|
200
|
+
progress: int = 0
|
|
201
|
+
latest_stage: str | None = None
|
|
202
|
+
latest_message: str | None = None
|
|
203
|
+
updated_at: datetime
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TaskRecord(BaseModel):
|
|
207
|
+
task_id: str = Field(default_factory=lambda: uuid4().hex)
|
|
208
|
+
video_id: str | None = None
|
|
209
|
+
status: TaskStatus = TaskStatus.QUEUED
|
|
210
|
+
task_input: TaskInput
|
|
211
|
+
page_number: int | None = None
|
|
212
|
+
page_title: str | None = None
|
|
213
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
214
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
215
|
+
result: TaskResult | None = None
|
|
216
|
+
error_code: str | None = None
|
|
217
|
+
error_message: str | None = None
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def task_duration_seconds(self) -> float | None:
|
|
221
|
+
if not self.created_at or not self.updated_at:
|
|
222
|
+
return None
|
|
223
|
+
return max(0.0, (self.updated_at - self.created_at).total_seconds())
|
|
224
|
+
|
|
225
|
+
def to_summary(self) -> TaskSummaryResponse:
|
|
226
|
+
page_number = self.page_number if self.page_number is not None else extract_bilibili_page(self.task_input.source)
|
|
227
|
+
return TaskSummaryResponse(
|
|
228
|
+
task_id=self.task_id,
|
|
229
|
+
video_id=self.video_id,
|
|
230
|
+
status=self.status,
|
|
231
|
+
input_type=self.task_input.input_type.value,
|
|
232
|
+
source=self.task_input.source,
|
|
233
|
+
title=self.task_input.title,
|
|
234
|
+
page_number=page_number,
|
|
235
|
+
page_title=self.page_title or self.task_input.title,
|
|
236
|
+
created_at=self.created_at,
|
|
237
|
+
updated_at=self.updated_at,
|
|
238
|
+
llm_total_tokens=self.result.llm_total_tokens if self.result else None,
|
|
239
|
+
task_duration_seconds=self.task_duration_seconds,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def to_detail(self) -> TaskDetailResponse:
|
|
243
|
+
return TaskDetailResponse(
|
|
244
|
+
**self.to_summary().model_dump(),
|
|
245
|
+
result=self.result,
|
|
246
|
+
error_code=self.error_code,
|
|
247
|
+
error_message=self.error_message,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TaskEventRecord(BaseModel):
|
|
252
|
+
event_id: str = Field(default_factory=lambda: uuid4().hex)
|
|
253
|
+
task_id: str
|
|
254
|
+
stage: str
|
|
255
|
+
progress: int = 0
|
|
256
|
+
message: str
|
|
257
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
258
|
+
payload: dict[str, object] = Field(default_factory=dict)
|
|
259
|
+
|
|
260
|
+
def to_response(self) -> TaskEventResponse:
|
|
261
|
+
return TaskEventResponse(
|
|
262
|
+
event_id=self.event_id,
|
|
263
|
+
task_id=self.task_id,
|
|
264
|
+
stage=self.stage,
|
|
265
|
+
progress=self.progress,
|
|
266
|
+
message=self.message,
|
|
267
|
+
created_at=self.created_at,
|
|
268
|
+
payload=self.payload,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class VideoTagRecord(BaseModel):
|
|
273
|
+
video_id: str
|
|
274
|
+
tag: str
|
|
275
|
+
source: str = "manual"
|
|
276
|
+
confidence: float = 1.0
|
|
277
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class KnowledgeIndexChunkRecord(BaseModel):
|
|
281
|
+
chunk_id: str
|
|
282
|
+
video_id: str
|
|
283
|
+
embedding_json: str
|
|
284
|
+
indexed_content: str
|
|
285
|
+
index_type: str = "video_summary"
|
|
286
|
+
segment_order: int | None = None
|
|
287
|
+
anchor_label: str | None = None
|
|
288
|
+
anchor_seconds: float | None = None
|
|
289
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
290
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class TagItem(BaseModel):
|
|
294
|
+
tag: str
|
|
295
|
+
count: int
|
|
296
|
+
videos: list[str] = Field(default_factory=list)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class TagListResponse(BaseModel):
|
|
300
|
+
items: list[TagItem] = Field(default_factory=list)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class VideoTagListResponse(BaseModel):
|
|
304
|
+
video_id: str
|
|
305
|
+
items: list[VideoTagRecord] = Field(default_factory=list)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class KnowledgeTagCreateRequest(BaseModel):
|
|
309
|
+
video_id: str
|
|
310
|
+
tag: str
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class KnowledgeAutoTagRequest(BaseModel):
|
|
314
|
+
video_ids: list[str] | None = None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class KnowledgeAutoTagVideoResult(BaseModel):
|
|
318
|
+
video_id: str
|
|
319
|
+
tags: list[str] = Field(default_factory=list)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class KnowledgeAutoTagResponse(BaseModel):
|
|
323
|
+
items: list[KnowledgeAutoTagVideoResult] = Field(default_factory=list)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class KnowledgeNetworkNode(BaseModel):
|
|
327
|
+
id: str
|
|
328
|
+
label: str
|
|
329
|
+
type: str
|
|
330
|
+
count: int | None = None
|
|
331
|
+
tags: list[str] = Field(default_factory=list)
|
|
332
|
+
degree: int | None = None
|
|
333
|
+
focus: bool = False
|
|
334
|
+
hidden_count: int = 0
|
|
335
|
+
video_count: int = 0
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class KnowledgeNetworkLink(BaseModel):
|
|
339
|
+
source: str
|
|
340
|
+
target: str
|
|
341
|
+
weight: float = 1.0
|
|
342
|
+
kind: str = "cooccurrence"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class KnowledgeNetworkResponse(BaseModel):
|
|
346
|
+
nodes: list[KnowledgeNetworkNode] = Field(default_factory=list)
|
|
347
|
+
links: list[KnowledgeNetworkLink] = Field(default_factory=list)
|
|
348
|
+
mode: str = "overview"
|
|
349
|
+
hidden_tag_count: int = 0
|
|
350
|
+
selected_tags: list[str] = Field(default_factory=list)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class KnowledgeSearchFilters(BaseModel):
|
|
354
|
+
tags: list[str] = Field(default_factory=list)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class KnowledgeSearchRequest(BaseModel):
|
|
358
|
+
query: str
|
|
359
|
+
limit: int = 10
|
|
360
|
+
filters: KnowledgeSearchFilters | None = None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class KnowledgeSearchResult(BaseModel):
|
|
364
|
+
video_id: str
|
|
365
|
+
title: str
|
|
366
|
+
relevance_score: float
|
|
367
|
+
snippet: str
|
|
368
|
+
tags: list[str] = Field(default_factory=list)
|
|
369
|
+
cover_url: str = ""
|
|
370
|
+
timestamp: str | None = None
|
|
371
|
+
video_title: str | None = None
|
|
372
|
+
page_title: str | None = None
|
|
373
|
+
page_number: int | None = None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class KnowledgeSearchResponse(BaseModel):
|
|
377
|
+
query: str
|
|
378
|
+
results: list[KnowledgeSearchResult] = Field(default_factory=list)
|
|
379
|
+
total: int = 0
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class KnowledgeChatHistoryItem(BaseModel):
|
|
383
|
+
role: str
|
|
384
|
+
content: str
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class KnowledgeAskRequest(BaseModel):
|
|
388
|
+
query: str
|
|
389
|
+
context_limit: int = 5
|
|
390
|
+
history: list[KnowledgeChatHistoryItem] = Field(default_factory=list)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class KnowledgeSourceRef(BaseModel):
|
|
394
|
+
video_id: str
|
|
395
|
+
title: str
|
|
396
|
+
relevance_score: float
|
|
397
|
+
timestamp: str | None = None
|
|
398
|
+
video_title: str | None = None
|
|
399
|
+
page_title: str | None = None
|
|
400
|
+
page_number: int | None = None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class KnowledgeAskResponse(BaseModel):
|
|
404
|
+
query: str
|
|
405
|
+
answer: str
|
|
406
|
+
sources: list[KnowledgeSourceRef] = Field(default_factory=list)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class KnowledgeStatsResponse(BaseModel):
|
|
410
|
+
video_count: int
|
|
411
|
+
indexed_chunk_count: int
|
|
412
|
+
tag_count: int
|
|
413
|
+
untagged_video_count: int
|
|
414
|
+
knowledge_llm_available: bool
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class KnowledgeRebuildResponse(BaseModel):
|
|
418
|
+
indexed_videos: int
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from video_sum_infra.config import (
|
|
9
|
+
DEFAULT_SUMMARY_SYSTEM_PROMPT,
|
|
10
|
+
DEFAULT_SUMMARY_USER_PROMPT_TEMPLATE,
|
|
11
|
+
LEGACY_SUMMARY_SYSTEM_PROMPT,
|
|
12
|
+
LEGACY_SUMMARY_USER_PROMPT_TEMPLATE,
|
|
13
|
+
PREVIOUS_DEFAULT_SUMMARY_SYSTEM_PROMPT,
|
|
14
|
+
PREVIOUS_DEFAULT_SUMMARY_USER_PROMPT_TEMPLATE,
|
|
15
|
+
ServiceSettings,
|
|
16
|
+
recommend_mindmap_concurrency,
|
|
17
|
+
recommend_task_concurrency,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SettingsUpdatePayload(BaseModel):
|
|
22
|
+
host: str | None = None
|
|
23
|
+
port: int | None = None
|
|
24
|
+
data_dir: str | None = None
|
|
25
|
+
cache_dir: str | None = None
|
|
26
|
+
tasks_dir: str | None = None
|
|
27
|
+
database_url: str | None = None
|
|
28
|
+
whisper_model: str | None = None
|
|
29
|
+
whisper_device: str | None = None
|
|
30
|
+
whisper_compute_type: str | None = None
|
|
31
|
+
transcription_provider: str | None = None
|
|
32
|
+
device_preference: str | None = None
|
|
33
|
+
compute_type: str | None = None
|
|
34
|
+
model_mode: str | None = None
|
|
35
|
+
fixed_model: str | None = None
|
|
36
|
+
siliconflow_asr_base_url: str | None = None
|
|
37
|
+
siliconflow_asr_model: str | None = None
|
|
38
|
+
siliconflow_asr_api_key: str | None = None
|
|
39
|
+
cuda_variant: str | None = None
|
|
40
|
+
runtime_channel: str | None = None
|
|
41
|
+
output_dir: str | None = None
|
|
42
|
+
preserve_temp_audio: bool | None = None
|
|
43
|
+
enable_cache: bool | None = None
|
|
44
|
+
language: str | None = None
|
|
45
|
+
summary_mode: str | None = None
|
|
46
|
+
llm_enabled: bool | None = None
|
|
47
|
+
auto_generate_mindmap: bool | None = None
|
|
48
|
+
llm_provider: str | None = None
|
|
49
|
+
llm_base_url: str | None = None
|
|
50
|
+
llm_model: str | None = None
|
|
51
|
+
llm_api_key: str | None = None
|
|
52
|
+
knowledge_llm_mode: str | None = None
|
|
53
|
+
knowledge_llm_enabled: bool | None = None
|
|
54
|
+
knowledge_llm_base_url: str | None = None
|
|
55
|
+
knowledge_llm_model: str | None = None
|
|
56
|
+
knowledge_llm_api_key: str | None = None
|
|
57
|
+
knowledge_index_auto_rebuild: str | None = None
|
|
58
|
+
knowledge_enabled: bool | None = None
|
|
59
|
+
summary_system_prompt: str | None = None
|
|
60
|
+
summary_user_prompt_template: str | None = None
|
|
61
|
+
summary_chunk_target_chars: int | None = None
|
|
62
|
+
summary_chunk_overlap_segments: int | None = None
|
|
63
|
+
task_concurrency: int | None = None
|
|
64
|
+
mindmap_concurrency: int | None = None
|
|
65
|
+
summary_chunk_concurrency: int | None = None
|
|
66
|
+
summary_chunk_retry_count: int | None = None
|
|
67
|
+
ytdlp_cookies_file: str | None = None
|
|
68
|
+
ytdlp_cookies_browser: str | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SettingsManager:
|
|
72
|
+
def __init__(self, base_settings: ServiceSettings) -> None:
|
|
73
|
+
self._base_settings = base_settings
|
|
74
|
+
self._settings = base_settings
|
|
75
|
+
self._settings_path = Path(base_settings.data_dir) / "settings.json"
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def current(self) -> ServiceSettings:
|
|
79
|
+
return self._settings
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def has_persisted_settings(self) -> bool:
|
|
83
|
+
return self._settings_path.exists()
|
|
84
|
+
|
|
85
|
+
def load(self) -> ServiceSettings:
|
|
86
|
+
if self._settings_path.exists():
|
|
87
|
+
stored = json.loads(self._settings_path.read_text(encoding="utf-8"))
|
|
88
|
+
if stored.get("summary_system_prompt") == LEGACY_SUMMARY_SYSTEM_PROMPT:
|
|
89
|
+
stored["summary_system_prompt"] = DEFAULT_SUMMARY_SYSTEM_PROMPT
|
|
90
|
+
if stored.get("summary_system_prompt") == PREVIOUS_DEFAULT_SUMMARY_SYSTEM_PROMPT:
|
|
91
|
+
stored["summary_system_prompt"] = DEFAULT_SUMMARY_SYSTEM_PROMPT
|
|
92
|
+
if stored.get("summary_user_prompt_template") == LEGACY_SUMMARY_USER_PROMPT_TEMPLATE:
|
|
93
|
+
stored["summary_user_prompt_template"] = DEFAULT_SUMMARY_USER_PROMPT_TEMPLATE
|
|
94
|
+
if stored.get("summary_user_prompt_template") == PREVIOUS_DEFAULT_SUMMARY_USER_PROMPT_TEMPLATE:
|
|
95
|
+
stored["summary_user_prompt_template"] = DEFAULT_SUMMARY_USER_PROMPT_TEMPLATE
|
|
96
|
+
migrated = False
|
|
97
|
+
candidate = ServiceSettings.model_validate({**self._base_settings.model_dump(), **stored})
|
|
98
|
+
if "task_concurrency" not in stored:
|
|
99
|
+
stored["task_concurrency"] = recommend_task_concurrency(candidate)
|
|
100
|
+
migrated = True
|
|
101
|
+
if "mindmap_concurrency" not in stored:
|
|
102
|
+
stored["mindmap_concurrency"] = recommend_mindmap_concurrency()
|
|
103
|
+
migrated = True
|
|
104
|
+
self._settings = ServiceSettings.model_validate({**self._base_settings.model_dump(), **stored})
|
|
105
|
+
if migrated:
|
|
106
|
+
self._settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
self._settings_path.write_text(
|
|
108
|
+
json.dumps(self._settings.model_dump(mode="json"), ensure_ascii=False, indent=2),
|
|
109
|
+
encoding="utf-8",
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
self._settings = self._base_settings.model_copy(
|
|
113
|
+
update={
|
|
114
|
+
"task_concurrency": recommend_task_concurrency(self._base_settings),
|
|
115
|
+
"mindmap_concurrency": recommend_mindmap_concurrency(),
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
return self._settings
|
|
119
|
+
|
|
120
|
+
def save(self, payload: SettingsUpdatePayload) -> ServiceSettings:
|
|
121
|
+
current_dump = self._settings.model_dump(mode="json")
|
|
122
|
+
updates = payload.model_dump(exclude_none=True)
|
|
123
|
+
next_settings = ServiceSettings.model_validate({**current_dump, **updates})
|
|
124
|
+
self._settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
self._settings_path.write_text(
|
|
126
|
+
json.dumps(next_settings.model_dump(mode="json"), ensure_ascii=False, indent=2),
|
|
127
|
+
encoding="utf-8",
|
|
128
|
+
)
|
|
129
|
+
self._settings = next_settings
|
|
130
|
+
return self._settings
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastapi import HTTPException
|
|
7
|
+
|
|
8
|
+
from video_sum_core.models.tasks import TaskMindMap
|
|
9
|
+
from video_sum_infra.config import ServiceSettings
|
|
10
|
+
|
|
11
|
+
from video_sum_service.schemas import TaskRecord, VideoAssetRecord
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("video_sum_service.app")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def task_artifact_directories(tasks: list[TaskRecord], tasks_dir: Path) -> list[Path]:
|
|
17
|
+
directories: set[Path] = set()
|
|
18
|
+
for task in tasks:
|
|
19
|
+
directories.add(tasks_dir / task.task_id)
|
|
20
|
+
if task.result is None:
|
|
21
|
+
continue
|
|
22
|
+
for artifact_path in task.result.artifacts.values():
|
|
23
|
+
try:
|
|
24
|
+
path = Path(str(artifact_path))
|
|
25
|
+
except (TypeError, ValueError):
|
|
26
|
+
continue
|
|
27
|
+
directories.add(path.parent)
|
|
28
|
+
return sorted(directories, key=lambda item: len(item.parts), reverse=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def video_cover_paths(video: VideoAssetRecord, cache_dir: Path) -> list[Path]:
|
|
32
|
+
cover_paths: set[Path] = set()
|
|
33
|
+
cover_url = str(video.cover_url or "")
|
|
34
|
+
if cover_url.startswith("/media/covers/"):
|
|
35
|
+
cover_paths.add(cache_dir / "covers" / Path(cover_url).name)
|
|
36
|
+
return sorted(cover_paths)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def video_source_paths(video: VideoAssetRecord, cache_dir: Path) -> list[Path]:
|
|
40
|
+
source_paths: set[Path] = set()
|
|
41
|
+
if str(video.platform or "").lower() != "local":
|
|
42
|
+
return []
|
|
43
|
+
try:
|
|
44
|
+
source_path = Path(str(video.source_url or "")).resolve()
|
|
45
|
+
uploads_dir = (cache_dir / "uploads").resolve()
|
|
46
|
+
if uploads_dir in source_path.parents:
|
|
47
|
+
source_paths.add(source_path)
|
|
48
|
+
except (OSError, RuntimeError, ValueError):
|
|
49
|
+
return []
|
|
50
|
+
return sorted(source_paths)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cleanup_video_files(video: VideoAssetRecord, tasks: list[TaskRecord], current_settings: ServiceSettings) -> None:
|
|
54
|
+
for directory in task_artifact_directories(tasks, current_settings.tasks_dir):
|
|
55
|
+
try:
|
|
56
|
+
if directory.exists():
|
|
57
|
+
shutil.rmtree(directory, ignore_errors=False)
|
|
58
|
+
except OSError:
|
|
59
|
+
logger.warning("failed to remove task directory: %s", directory, exc_info=True)
|
|
60
|
+
|
|
61
|
+
for cover_path in video_cover_paths(video, current_settings.cache_dir):
|
|
62
|
+
try:
|
|
63
|
+
if cover_path.exists():
|
|
64
|
+
cover_path.unlink()
|
|
65
|
+
except OSError:
|
|
66
|
+
logger.warning("failed to remove cached cover: %s", cover_path, exc_info=True)
|
|
67
|
+
|
|
68
|
+
for source_path in video_source_paths(video, current_settings.cache_dir):
|
|
69
|
+
try:
|
|
70
|
+
if source_path.exists():
|
|
71
|
+
source_path.unlink()
|
|
72
|
+
except OSError:
|
|
73
|
+
logger.warning("failed to remove cached local media source: %s", source_path, exc_info=True)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cleanup_task_files(task: TaskRecord, current_settings: ServiceSettings) -> None:
|
|
77
|
+
for directory in task_artifact_directories([task], current_settings.tasks_dir):
|
|
78
|
+
try:
|
|
79
|
+
if directory.exists():
|
|
80
|
+
shutil.rmtree(directory, ignore_errors=False)
|
|
81
|
+
except OSError:
|
|
82
|
+
logger.warning("failed to remove task directory: %s", directory, exc_info=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_task_segments(summary_path: str) -> list[dict[str, object]]:
|
|
86
|
+
try:
|
|
87
|
+
payload = json.loads(Path(summary_path).read_text(encoding="utf-8"))
|
|
88
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
89
|
+
raise HTTPException(status_code=500, detail="无法读取当前任务的分段结果。") from exc
|
|
90
|
+
|
|
91
|
+
segments = payload.get("segments")
|
|
92
|
+
if not isinstance(segments, list) or not segments:
|
|
93
|
+
raise HTTPException(status_code=400, detail="当前任务缺少可复用的分段数据。")
|
|
94
|
+
return segments
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_task_mindmap(mindmap_path: str) -> TaskMindMap:
|
|
98
|
+
try:
|
|
99
|
+
payload = json.loads(Path(mindmap_path).read_text(encoding="utf-8"))
|
|
100
|
+
return TaskMindMap.model_validate(payload)
|
|
101
|
+
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
102
|
+
raise HTTPException(status_code=500, detail="无法读取当前任务的思维导图结果。") from exc
|