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 @@
|
|
|
1
|
+
"""FastAPI route groups for the service."""
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from queue import Empty, Queue
|
|
6
|
+
from threading import BoundedSemaphore, Event, Thread
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
9
|
+
from fastapi.responses import StreamingResponse
|
|
10
|
+
|
|
11
|
+
from video_sum_service.context import settings_manager
|
|
12
|
+
from video_sum_service.runtime_support import detect_environment
|
|
13
|
+
from video_sum_service.knowledge import KnowledgeIndexService, RagService, TagService
|
|
14
|
+
from video_sum_service.knowledge.local_llm import knowledge_llm_available
|
|
15
|
+
from video_sum_service.repository import SqliteTaskRepository
|
|
16
|
+
from video_sum_service.schemas import (
|
|
17
|
+
KnowledgeAskRequest,
|
|
18
|
+
KnowledgeAskResponse,
|
|
19
|
+
KnowledgeAutoTagRequest,
|
|
20
|
+
KnowledgeAutoTagResponse,
|
|
21
|
+
KnowledgeNetworkResponse,
|
|
22
|
+
KnowledgeRebuildResponse,
|
|
23
|
+
KnowledgeSearchRequest,
|
|
24
|
+
KnowledgeSearchResponse,
|
|
25
|
+
KnowledgeStatsResponse,
|
|
26
|
+
KnowledgeTagCreateRequest,
|
|
27
|
+
TagListResponse,
|
|
28
|
+
VideoTagListResponse,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
router = APIRouter(prefix="/api/v1/knowledge")
|
|
32
|
+
_QUEUE_TIMEOUT = object()
|
|
33
|
+
_ASK_STREAM_SEMAPHORE = BoundedSemaphore(2)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_queue_item(event_queue: Queue[object], timeout: float) -> object:
|
|
37
|
+
try:
|
|
38
|
+
return event_queue.get(timeout=timeout)
|
|
39
|
+
except Empty:
|
|
40
|
+
return _QUEUE_TIMEOUT
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _knowledge_settings_signature(settings) -> tuple[object, ...]:
|
|
44
|
+
return (
|
|
45
|
+
bool(getattr(settings, "knowledge_enabled", False)),
|
|
46
|
+
str(getattr(settings, "runtime_channel", "base") or "base"),
|
|
47
|
+
str(getattr(settings, "knowledge_index_auto_rebuild", "disabled") or "disabled"),
|
|
48
|
+
bool(settings.llm_enabled),
|
|
49
|
+
str(settings.llm_base_url or ""),
|
|
50
|
+
str(settings.llm_model or ""),
|
|
51
|
+
str(settings.llm_api_key or ""),
|
|
52
|
+
str(getattr(settings, "knowledge_llm_mode", "same_as_main") or "same_as_main"),
|
|
53
|
+
bool(getattr(settings, "knowledge_llm_enabled", False)),
|
|
54
|
+
str(getattr(settings, "knowledge_llm_base_url", "") or ""),
|
|
55
|
+
str(getattr(settings, "knowledge_llm_model", "") or ""),
|
|
56
|
+
str(getattr(settings, "knowledge_llm_api_key", "") or ""),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_services(request: Request) -> tuple[TagService, KnowledgeIndexService, RagService]:
|
|
61
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
62
|
+
settings = settings_manager.current
|
|
63
|
+
settings_signature = _knowledge_settings_signature(settings)
|
|
64
|
+
tag_service = getattr(request.app.state, "knowledge_tag_service", None)
|
|
65
|
+
index_service = getattr(request.app.state, "knowledge_index_service", None)
|
|
66
|
+
rag_service = getattr(request.app.state, "knowledge_rag_service", None)
|
|
67
|
+
cached_signature = getattr(request.app.state, "knowledge_settings_signature", None)
|
|
68
|
+
|
|
69
|
+
if tag_service is None or index_service is None or rag_service is None or cached_signature != settings_signature:
|
|
70
|
+
tag_service = TagService(task_store, settings)
|
|
71
|
+
index_service = KnowledgeIndexService(task_store, settings)
|
|
72
|
+
rag_service = RagService(task_store, index_service, tag_service, settings)
|
|
73
|
+
request.app.state.knowledge_tag_service = tag_service
|
|
74
|
+
request.app.state.knowledge_index_service = index_service
|
|
75
|
+
request.app.state.knowledge_rag_service = rag_service
|
|
76
|
+
request.app.state.knowledge_settings_signature = settings_signature
|
|
77
|
+
|
|
78
|
+
return tag_service, index_service, rag_service
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _knowledge_runtime_ready() -> bool:
|
|
82
|
+
environment = detect_environment(settings_manager.current.runtime_channel)
|
|
83
|
+
return bool(environment.get("knowledgeDependenciesReady"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _knowledge_enabled() -> bool:
|
|
87
|
+
return bool(getattr(settings_manager.current, "knowledge_enabled", False))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _index_video_if_ready(index_service: KnowledgeIndexService, video_id: str) -> None:
|
|
91
|
+
if _knowledge_enabled() and _knowledge_runtime_ready():
|
|
92
|
+
index_service.index_video(video_id)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _require_knowledge_enabled() -> None:
|
|
96
|
+
if not _knowledge_enabled():
|
|
97
|
+
raise HTTPException(
|
|
98
|
+
status_code=400,
|
|
99
|
+
detail="知识库当前未启用。请先在设置中的知识库板块开启知识库。",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _require_knowledge_runtime() -> None:
|
|
104
|
+
_require_knowledge_enabled()
|
|
105
|
+
if not _knowledge_runtime_ready():
|
|
106
|
+
raise HTTPException(
|
|
107
|
+
status_code=424,
|
|
108
|
+
detail="知识库依赖未安装。请先到设置中的知识库或运行时板块安装知识库依赖。",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.get("/tags", response_model=TagListResponse | VideoTagListResponse)
|
|
113
|
+
def get_tags(request: Request, video_id: str | None = None) -> TagListResponse | VideoTagListResponse:
|
|
114
|
+
tag_service, _index_service, _rag_service = _get_services(request)
|
|
115
|
+
if video_id:
|
|
116
|
+
return VideoTagListResponse(video_id=video_id, items=tag_service.get_tags_for_video(video_id))
|
|
117
|
+
return TagListResponse(items=tag_service.get_all_tags())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@router.post("/tags", response_model=VideoTagListResponse)
|
|
121
|
+
def create_tag(body: KnowledgeTagCreateRequest, request: Request) -> VideoTagListResponse:
|
|
122
|
+
tag_service, index_service, _rag_service = _get_services(request)
|
|
123
|
+
created = tag_service.add_tag(body.video_id, body.tag)
|
|
124
|
+
if not created:
|
|
125
|
+
raise HTTPException(status_code=404, detail="Video not found.")
|
|
126
|
+
_index_video_if_ready(index_service, body.video_id)
|
|
127
|
+
return VideoTagListResponse(video_id=body.video_id, items=tag_service.get_tags_for_video(body.video_id))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@router.delete("/tags/{video_id}/{tag}", response_model=VideoTagListResponse)
|
|
131
|
+
def delete_tag(video_id: str, tag: str, request: Request) -> VideoTagListResponse:
|
|
132
|
+
tag_service, index_service, _rag_service = _get_services(request)
|
|
133
|
+
removed = tag_service.remove_tag(video_id, tag)
|
|
134
|
+
if not removed:
|
|
135
|
+
raise HTTPException(status_code=404, detail="Tag not found.")
|
|
136
|
+
_index_video_if_ready(index_service, video_id)
|
|
137
|
+
return VideoTagListResponse(video_id=video_id, items=tag_service.get_tags_for_video(video_id))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.post("/auto-tag", response_model=KnowledgeAutoTagResponse)
|
|
141
|
+
def auto_tag(body: KnowledgeAutoTagRequest, request: Request) -> KnowledgeAutoTagResponse:
|
|
142
|
+
tag_service, index_service, _rag_service = _get_services(request)
|
|
143
|
+
response = tag_service.batch_auto_tag(body.video_ids)
|
|
144
|
+
for item in response.items:
|
|
145
|
+
_index_video_if_ready(index_service, item.video_id)
|
|
146
|
+
return response
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@router.get("/network", response_model=KnowledgeNetworkResponse)
|
|
150
|
+
def get_network(
|
|
151
|
+
request: Request,
|
|
152
|
+
selected_tag: list[str] | None = Query(default=None),
|
|
153
|
+
max_tags: int = 12,
|
|
154
|
+
max_videos: int = 8,
|
|
155
|
+
) -> KnowledgeNetworkResponse:
|
|
156
|
+
tag_service, _index_service, _rag_service = _get_services(request)
|
|
157
|
+
selected_values = selected_tag if isinstance(selected_tag, list) else None
|
|
158
|
+
return tag_service.get_network_data(selected_values, max_tags=max_tags, max_videos=max_videos)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.post("/search", response_model=KnowledgeSearchResponse)
|
|
162
|
+
def search_knowledge(body: KnowledgeSearchRequest, request: Request) -> KnowledgeSearchResponse:
|
|
163
|
+
_require_knowledge_runtime()
|
|
164
|
+
_tag_service, index_service, _rag_service = _get_services(request)
|
|
165
|
+
filters = body.filters.tags if body.filters is not None else []
|
|
166
|
+
results = index_service.search(body.query, limit=body.limit, tag_filter=filters)
|
|
167
|
+
return KnowledgeSearchResponse(query=body.query, results=results, total=len(results))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@router.post("/ask", response_model=KnowledgeAskResponse)
|
|
171
|
+
def ask_knowledge(body: KnowledgeAskRequest, request: Request) -> KnowledgeAskResponse:
|
|
172
|
+
_require_knowledge_runtime()
|
|
173
|
+
_tag_service, _index_service, rag_service = _get_services(request)
|
|
174
|
+
return rag_service.ask(
|
|
175
|
+
body.query,
|
|
176
|
+
context_limit=body.context_limit,
|
|
177
|
+
history=getattr(body, "history", []),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@router.post("/ask/stream")
|
|
182
|
+
async def ask_knowledge_stream(body: KnowledgeAskRequest, request: Request) -> StreamingResponse:
|
|
183
|
+
_require_knowledge_runtime()
|
|
184
|
+
_tag_service, _index_service, rag_service = _get_services(request)
|
|
185
|
+
|
|
186
|
+
async def event_generator():
|
|
187
|
+
event_queue: Queue[object] = Queue()
|
|
188
|
+
cancel_event = Event()
|
|
189
|
+
|
|
190
|
+
def produce_events() -> None:
|
|
191
|
+
acquired = _ASK_STREAM_SEMAPHORE.acquire(blocking=False)
|
|
192
|
+
if not acquired:
|
|
193
|
+
event_queue.put(
|
|
194
|
+
HTTPException(
|
|
195
|
+
status_code=429,
|
|
196
|
+
detail="知识库问答任务较多,请等待当前回答结束后再试。",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
event_queue.put(None)
|
|
200
|
+
return
|
|
201
|
+
try:
|
|
202
|
+
for event in rag_service.ask_stream(
|
|
203
|
+
body.query,
|
|
204
|
+
context_limit=body.context_limit,
|
|
205
|
+
history=getattr(body, "history", []),
|
|
206
|
+
should_cancel=cancel_event.is_set,
|
|
207
|
+
):
|
|
208
|
+
if cancel_event.is_set():
|
|
209
|
+
return
|
|
210
|
+
event_queue.put(event)
|
|
211
|
+
except Exception as exc: # pragma: no cover - surfaced to the SSE client below
|
|
212
|
+
event_queue.put(exc)
|
|
213
|
+
finally:
|
|
214
|
+
_ASK_STREAM_SEMAPHORE.release()
|
|
215
|
+
event_queue.put(None)
|
|
216
|
+
|
|
217
|
+
worker = Thread(target=produce_events, daemon=True, name="knowledge-ask-stream")
|
|
218
|
+
worker.start()
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
while True:
|
|
222
|
+
if await request.is_disconnected():
|
|
223
|
+
cancel_event.set()
|
|
224
|
+
return
|
|
225
|
+
item = await asyncio.to_thread(_get_queue_item, event_queue, 1.5)
|
|
226
|
+
if item is _QUEUE_TIMEOUT:
|
|
227
|
+
yield ": keep-alive\n\n"
|
|
228
|
+
continue
|
|
229
|
+
if item is None:
|
|
230
|
+
return
|
|
231
|
+
if isinstance(item, HTTPException):
|
|
232
|
+
raise item
|
|
233
|
+
if isinstance(item, Exception):
|
|
234
|
+
raise item
|
|
235
|
+
event_name, payload = item
|
|
236
|
+
yield f"event: {event_name}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
|
237
|
+
except HTTPException as exc:
|
|
238
|
+
yield (
|
|
239
|
+
"event: error\n"
|
|
240
|
+
f"data: {json.dumps({'message': str(exc.detail), 'status_code': exc.status_code}, ensure_ascii=False)}\n\n"
|
|
241
|
+
)
|
|
242
|
+
except Exception as exc: # pragma: no cover - defensive fallback
|
|
243
|
+
yield f"event: error\ndata: {json.dumps({'message': f'知识库问答失败:{exc}'}, ensure_ascii=False)}\n\n"
|
|
244
|
+
finally:
|
|
245
|
+
cancel_event.set()
|
|
246
|
+
|
|
247
|
+
return StreamingResponse(
|
|
248
|
+
event_generator(),
|
|
249
|
+
media_type="text/event-stream",
|
|
250
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@router.get("/stats", response_model=KnowledgeStatsResponse)
|
|
255
|
+
def get_knowledge_stats(request: Request) -> KnowledgeStatsResponse:
|
|
256
|
+
tag_service, index_service, _rag_service = _get_services(request)
|
|
257
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
258
|
+
settings = settings_manager.current
|
|
259
|
+
return KnowledgeStatsResponse(
|
|
260
|
+
video_count=len(task_store.list_video_assets()),
|
|
261
|
+
indexed_chunk_count=task_store.get_knowledge_chunk_count(),
|
|
262
|
+
tag_count=len(tag_service.get_all_tags()),
|
|
263
|
+
untagged_video_count=len(task_store.list_untagged_video_ids()),
|
|
264
|
+
knowledge_llm_available=bool(settings.knowledge_enabled and knowledge_llm_available(settings)),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@router.post("/rebuild-index", response_model=KnowledgeRebuildResponse)
|
|
269
|
+
def rebuild_index(request: Request) -> KnowledgeRebuildResponse:
|
|
270
|
+
_require_knowledge_runtime()
|
|
271
|
+
_tag_service, index_service, _rag_service = _get_services(request)
|
|
272
|
+
return KnowledgeRebuildResponse(indexed_videos=index_service.rebuild_index(force=True))
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
7
|
+
|
|
8
|
+
from video_sum_core.models.tasks import TaskStatus
|
|
9
|
+
from video_sum_infra.runtime import (
|
|
10
|
+
activate_runtime_pythonpath,
|
|
11
|
+
bootstrap_managed_runtime,
|
|
12
|
+
log_dir,
|
|
13
|
+
prepend_runtime_path,
|
|
14
|
+
service_log_path,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from video_sum_service.context import app_info, logger, settings_manager
|
|
18
|
+
from video_sum_service.integrations import (
|
|
19
|
+
extract_http_error_detail,
|
|
20
|
+
probe_asr_connection,
|
|
21
|
+
probe_llm_connection,
|
|
22
|
+
read_log_tail,
|
|
23
|
+
)
|
|
24
|
+
from video_sum_service.runtime_support import (
|
|
25
|
+
build_worker,
|
|
26
|
+
clear_environment_probe_cache,
|
|
27
|
+
detect_environment,
|
|
28
|
+
inspect_runtime_channels,
|
|
29
|
+
install_cuda_support,
|
|
30
|
+
install_knowledge_dependencies,
|
|
31
|
+
install_local_asr,
|
|
32
|
+
replace_task_worker,
|
|
33
|
+
serialize_settings,
|
|
34
|
+
sync_all_runtime_channels,
|
|
35
|
+
sync_runtime_channel,
|
|
36
|
+
)
|
|
37
|
+
from video_sum_service.settings_manager import SettingsUpdatePayload
|
|
38
|
+
|
|
39
|
+
router = APIRouter(prefix="/api/v1")
|
|
40
|
+
LATEST_RELEASE_URL = "https://api.github.com/repos/lycohana/BiliSum/releases/latest"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _clear_knowledge_service_cache(app_state) -> None:
|
|
44
|
+
for key in (
|
|
45
|
+
"knowledge_tag_service",
|
|
46
|
+
"knowledge_index_service",
|
|
47
|
+
"knowledge_rag_service",
|
|
48
|
+
"knowledge_settings_signature",
|
|
49
|
+
):
|
|
50
|
+
if hasattr(app_state, key):
|
|
51
|
+
delattr(app_state, key)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalize_version(value: str | None) -> str:
|
|
55
|
+
return str(value or "").strip().removeprefix("v").removeprefix("V")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _version_key(value: str | None) -> tuple[int | str, ...]:
|
|
59
|
+
normalized = _normalize_version(value)
|
|
60
|
+
if not normalized:
|
|
61
|
+
return (0,)
|
|
62
|
+
|
|
63
|
+
parts: list[int | str] = []
|
|
64
|
+
for chunk in re.split(r"[.\-+_]", normalized):
|
|
65
|
+
if not chunk:
|
|
66
|
+
continue
|
|
67
|
+
parts.append(int(chunk) if chunk.isdigit() else chunk.lower())
|
|
68
|
+
return tuple(parts) or (0,)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.get("/system/info")
|
|
72
|
+
def system_info(runtime_channel: str | None = None, refresh: bool = False) -> dict[str, object]:
|
|
73
|
+
current_settings = settings_manager.current
|
|
74
|
+
active_channel = runtime_channel or current_settings.runtime_channel
|
|
75
|
+
if refresh:
|
|
76
|
+
clear_environment_probe_cache(active_channel)
|
|
77
|
+
environment = detect_environment(active_channel)
|
|
78
|
+
runtime_settings = current_settings.with_resolved_runtime(
|
|
79
|
+
cuda_available=bool(environment.get("cudaAvailable"))
|
|
80
|
+
)
|
|
81
|
+
return {
|
|
82
|
+
"application": {"name": app_info.name, "version": app_info.version},
|
|
83
|
+
"service": {
|
|
84
|
+
"host": current_settings.host,
|
|
85
|
+
"port": current_settings.port,
|
|
86
|
+
"data_dir": str(current_settings.data_dir),
|
|
87
|
+
"cache_dir": str(current_settings.cache_dir),
|
|
88
|
+
"tasks_dir": str(current_settings.tasks_dir),
|
|
89
|
+
"database_url": current_settings.database_url,
|
|
90
|
+
"log_dir": str(log_dir()),
|
|
91
|
+
"log_file": str(service_log_path()),
|
|
92
|
+
},
|
|
93
|
+
"runtime": {
|
|
94
|
+
"runtime_channel": active_channel,
|
|
95
|
+
"whisper_model": runtime_settings.whisper_model,
|
|
96
|
+
"whisper_device": runtime_settings.whisper_device,
|
|
97
|
+
"whisper_compute_type": runtime_settings.whisper_compute_type,
|
|
98
|
+
"llm_enabled": current_settings.llm_enabled,
|
|
99
|
+
"llm_model": current_settings.llm_model,
|
|
100
|
+
},
|
|
101
|
+
"taskModel": {"statuses": [status.value for status in TaskStatus]},
|
|
102
|
+
"environment": environment,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@router.get("/settings")
|
|
107
|
+
def get_settings() -> dict[str, object]:
|
|
108
|
+
return serialize_settings(settings_manager.current)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.get("/app/update")
|
|
112
|
+
def get_app_update() -> dict[str, object]:
|
|
113
|
+
current_version = app_info.version
|
|
114
|
+
headers = {
|
|
115
|
+
"Accept": "application/vnd.github+json",
|
|
116
|
+
"User-Agent": f"{app_info.name}/{current_version}",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
with httpx.Client(timeout=20, follow_redirects=True, headers=headers) as client:
|
|
121
|
+
response = client.get(LATEST_RELEASE_URL)
|
|
122
|
+
except httpx.HTTPError as exc:
|
|
123
|
+
raise HTTPException(status_code=502, detail=f"获取最新版本信息失败:{exc}") from exc
|
|
124
|
+
|
|
125
|
+
if response.status_code >= 400:
|
|
126
|
+
detail = extract_http_error_detail(response)
|
|
127
|
+
raise HTTPException(status_code=response.status_code, detail=f"获取最新版本信息失败:{detail}")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
payload = response.json()
|
|
131
|
+
except ValueError as exc:
|
|
132
|
+
raise HTTPException(status_code=502, detail="获取最新版本信息失败:响应格式无效。") from exc
|
|
133
|
+
|
|
134
|
+
latest_version = _normalize_version(payload.get("tag_name") or payload.get("name") or current_version)
|
|
135
|
+
if not latest_version:
|
|
136
|
+
raise HTTPException(status_code=502, detail="获取最新版本信息失败:缺少版本号。")
|
|
137
|
+
|
|
138
|
+
current_key = _version_key(current_version)
|
|
139
|
+
latest_key = _version_key(latest_version)
|
|
140
|
+
is_newer_available = latest_key > current_key
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"status": "available" if is_newer_available else "not-available",
|
|
144
|
+
"version": latest_version if is_newer_available else _normalize_version(current_version),
|
|
145
|
+
"releaseDate": payload.get("published_at") or payload.get("created_at") or "",
|
|
146
|
+
"releaseNotes": str(payload.get("body") or "").strip() or None,
|
|
147
|
+
"downloadProgress": 0,
|
|
148
|
+
"errorMessage": None,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.put("/settings")
|
|
153
|
+
def update_settings(payload: SettingsUpdatePayload, request: Request) -> dict[str, object]:
|
|
154
|
+
previous_settings = settings_manager.current
|
|
155
|
+
current_settings = settings_manager.save(payload)
|
|
156
|
+
bootstrap_managed_runtime(current_settings.runtime_channel)
|
|
157
|
+
prepend_runtime_path(current_settings.runtime_channel)
|
|
158
|
+
activate_runtime_pythonpath(current_settings.runtime_channel)
|
|
159
|
+
current_settings.data_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
current_settings.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
current_settings.tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
runtime_channel_changed = previous_settings.runtime_channel != current_settings.runtime_channel
|
|
163
|
+
if runtime_channel_changed:
|
|
164
|
+
clear_environment_probe_cache(previous_settings.runtime_channel)
|
|
165
|
+
clear_environment_probe_cache(current_settings.runtime_channel)
|
|
166
|
+
_clear_knowledge_service_cache(request.app.state)
|
|
167
|
+
environment = detect_environment(current_settings.runtime_channel)
|
|
168
|
+
replace_task_worker(
|
|
169
|
+
request.app.state,
|
|
170
|
+
build_worker(
|
|
171
|
+
request.app.state.task_repository,
|
|
172
|
+
current_settings,
|
|
173
|
+
environment_info=environment,
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
return {
|
|
177
|
+
"saved": True,
|
|
178
|
+
"settings": serialize_settings(current_settings, environment_info=environment),
|
|
179
|
+
"message": "设置已保存。涉及服务监听地址的修改将在下次启动后生效。",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@router.post("/llm/test")
|
|
184
|
+
def post_llm_test(payload: SettingsUpdatePayload | None = None) -> dict[str, object]:
|
|
185
|
+
return probe_llm_connection(payload)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@router.post("/asr/test")
|
|
189
|
+
def post_asr_test(payload: SettingsUpdatePayload | None = None) -> dict[str, object]:
|
|
190
|
+
return probe_asr_connection(payload)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@router.get("/environment")
|
|
194
|
+
def get_environment(runtime_channel: str | None = None, refresh: bool = False) -> dict[str, object]:
|
|
195
|
+
active_channel = runtime_channel or settings_manager.current.runtime_channel
|
|
196
|
+
if refresh:
|
|
197
|
+
clear_environment_probe_cache(active_channel)
|
|
198
|
+
return detect_environment(active_channel)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@router.get("/runtime/status")
|
|
202
|
+
def get_runtime_status() -> dict[str, object]:
|
|
203
|
+
return inspect_runtime_channels()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@router.post("/runtime/sync")
|
|
207
|
+
def post_runtime_sync(request: Request, payload: dict[str, object] | None = None) -> dict[str, object]:
|
|
208
|
+
requested_channel = str((payload or {}).get("runtime_channel") or (payload or {}).get("runtimeChannel") or "").strip()
|
|
209
|
+
try:
|
|
210
|
+
result = sync_runtime_channel(requested_channel) if requested_channel else sync_all_runtime_channels()
|
|
211
|
+
_clear_knowledge_service_cache(request.app.state)
|
|
212
|
+
|
|
213
|
+
current_settings = settings_manager.current
|
|
214
|
+
environment = detect_environment(current_settings.runtime_channel)
|
|
215
|
+
replace_task_worker(
|
|
216
|
+
request.app.state,
|
|
217
|
+
build_worker(
|
|
218
|
+
request.app.state.task_repository,
|
|
219
|
+
current_settings,
|
|
220
|
+
environment_info=environment,
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
return {
|
|
224
|
+
**result,
|
|
225
|
+
"environment": environment,
|
|
226
|
+
"runtimeStatus": inspect_runtime_channels(),
|
|
227
|
+
}
|
|
228
|
+
except HTTPException:
|
|
229
|
+
raise
|
|
230
|
+
except Exception as exc:
|
|
231
|
+
logger.exception("runtime sync failed requested_channel=%s error=%s", requested_channel or "all", exc)
|
|
232
|
+
raise HTTPException(status_code=500, detail=f"运行时同步失败:{exc}") from exc
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@router.get("/system/logs")
|
|
236
|
+
def get_system_logs(lines: int = 200) -> dict[str, object]:
|
|
237
|
+
line_count = max(20, min(int(lines), 1000))
|
|
238
|
+
return {
|
|
239
|
+
"path": str(service_log_path()),
|
|
240
|
+
"lines": line_count,
|
|
241
|
+
"content": read_log_tail(line_count),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@router.post("/system/shutdown")
|
|
246
|
+
def shutdown_service(request: Request) -> dict[str, object]:
|
|
247
|
+
def shutdown() -> None:
|
|
248
|
+
task_worker = getattr(request.app.state, "task_worker", None)
|
|
249
|
+
if task_worker is not None:
|
|
250
|
+
task_worker.shutdown(wait=False)
|
|
251
|
+
os._exit(0)
|
|
252
|
+
|
|
253
|
+
threading.Timer(0.5, shutdown).start()
|
|
254
|
+
return {"shuttingDown": True, "message": "服务正在关闭。"}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@router.post("/cuda/install")
|
|
258
|
+
def post_cuda_install(payload: dict[str, object], request: Request) -> dict[str, object]:
|
|
259
|
+
requested_variant = payload.get("cuda_variant", payload.get("cudaVariant", "cu128"))
|
|
260
|
+
result, worker = install_cuda_support(str(requested_variant), request.app.state.task_repository)
|
|
261
|
+
_clear_knowledge_service_cache(request.app.state)
|
|
262
|
+
replace_task_worker(request.app.state, worker)
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@router.post("/asr/local/install")
|
|
267
|
+
def post_local_asr_install(request: Request, payload: dict[str, object] | None = None) -> dict[str, object]:
|
|
268
|
+
reinstall = bool((payload or {}).get("reinstall"))
|
|
269
|
+
result, worker = install_local_asr(reinstall=reinstall, repository=request.app.state.task_repository)
|
|
270
|
+
replace_task_worker(request.app.state, worker)
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@router.post("/knowledge/install")
|
|
275
|
+
def post_knowledge_install(request: Request, payload: dict[str, object] | None = None) -> dict[str, object]:
|
|
276
|
+
reinstall = bool((payload or {}).get("reinstall"))
|
|
277
|
+
result, worker = install_knowledge_dependencies(reinstall=reinstall, repository=request.app.state.task_repository)
|
|
278
|
+
_clear_knowledge_service_cache(request.app.state)
|
|
279
|
+
replace_task_worker(request.app.state, worker)
|
|
280
|
+
return result
|