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,480 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import importlib
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, HTTPException
|
|
10
|
+
from fastapi.responses import FileResponse
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
from yt_dlp import YoutubeDL
|
|
13
|
+
|
|
14
|
+
from video_sum_core.models.tasks import TaskStatus
|
|
15
|
+
from video_sum_infra.db import connect_sqlite
|
|
16
|
+
from video_sum_infra.runtime import (
|
|
17
|
+
activate_runtime_pythonpath,
|
|
18
|
+
bootstrap_managed_runtime,
|
|
19
|
+
is_frozen,
|
|
20
|
+
prepend_runtime_path,
|
|
21
|
+
repo_root,
|
|
22
|
+
runtime_python_executable,
|
|
23
|
+
write_runtime_metadata,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from video_sum_service.context import CACHE_STATIC_DIR, WEB_STATIC_DIR, app_info, logger, settings_manager
|
|
27
|
+
from video_sum_service.integrations import cache_cover_image, probe_asr_connection, probe_llm_connection
|
|
28
|
+
from video_sum_service.repository import SqliteTaskRepository
|
|
29
|
+
from video_sum_service.routers.system import router as system_router
|
|
30
|
+
from video_sum_service.routers.knowledge import router as knowledge_router
|
|
31
|
+
from video_sum_service.routers.tasks import router as tasks_router
|
|
32
|
+
from video_sum_service.routers.videos import router as videos_router
|
|
33
|
+
from video_sum_service.runtime_support import (
|
|
34
|
+
build_worker,
|
|
35
|
+
clear_environment_probe_cache,
|
|
36
|
+
detect_environment,
|
|
37
|
+
ensure_runtime_channel,
|
|
38
|
+
ensure_runtime_pip,
|
|
39
|
+
install_workspace_packages,
|
|
40
|
+
pip_install_with_fallbacks,
|
|
41
|
+
replace_task_worker,
|
|
42
|
+
run_command,
|
|
43
|
+
serialize_settings,
|
|
44
|
+
)
|
|
45
|
+
from video_sum_service.schemas import TaskMindMapResponse
|
|
46
|
+
from video_sum_service.settings_manager import SettingsUpdatePayload
|
|
47
|
+
from video_sum_service.task_artifacts import cleanup_video_files, load_task_mindmap
|
|
48
|
+
import video_sum_service.video_assets as video_assets
|
|
49
|
+
|
|
50
|
+
probe_video_asset = video_assets.probe_video_asset
|
|
51
|
+
|
|
52
|
+
_cleanup_video_files = cleanup_video_files
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _uses_current_service_python(runtime_channel: str) -> bool:
|
|
56
|
+
return not is_frozen() and runtime_channel == "base"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _run_host_command(command: list[str], timeout: int = 3600):
|
|
60
|
+
env = dict(os.environ)
|
|
61
|
+
for key in ("PYTHONHOME", "PYTHONPATH", "PYTHONEXECUTABLE", "__PYVENV_LAUNCHER__"):
|
|
62
|
+
env.pop(key, None)
|
|
63
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
64
|
+
env["PYTHONUTF8"] = "1"
|
|
65
|
+
return subprocess.run(
|
|
66
|
+
command,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
encoding="utf-8",
|
|
70
|
+
errors="replace",
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
check=True,
|
|
73
|
+
env=env,
|
|
74
|
+
cwd=repo_root(),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def recover_incomplete_tasks(repository: SqliteTaskRepository, task_worker) -> int:
|
|
79
|
+
recoverable = sorted(
|
|
80
|
+
(
|
|
81
|
+
record
|
|
82
|
+
for record in repository.list_tasks()
|
|
83
|
+
if record.status in {TaskStatus.QUEUED, TaskStatus.RUNNING}
|
|
84
|
+
),
|
|
85
|
+
key=lambda record: (record.created_at, record.task_id),
|
|
86
|
+
)
|
|
87
|
+
if not recoverable:
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
recovered_count = 0
|
|
91
|
+
for record in recoverable:
|
|
92
|
+
if record.status == TaskStatus.RUNNING:
|
|
93
|
+
logger.warning("recover interrupted running task task_id=%s video_id=%s", record.task_id, record.video_id)
|
|
94
|
+
else:
|
|
95
|
+
logger.info("recover queued task task_id=%s video_id=%s", record.task_id, record.video_id)
|
|
96
|
+
task_worker.submit(record)
|
|
97
|
+
recovered_count += 1
|
|
98
|
+
|
|
99
|
+
logger.info("recovered incomplete tasks count=%d", recovered_count)
|
|
100
|
+
return recovered_count
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@asynccontextmanager
|
|
104
|
+
async def lifespan(app: FastAPI):
|
|
105
|
+
current_settings = settings_manager.current
|
|
106
|
+
bootstrap_managed_runtime(current_settings.runtime_channel)
|
|
107
|
+
prepend_runtime_path(current_settings.runtime_channel)
|
|
108
|
+
activate_runtime_pythonpath(current_settings.runtime_channel)
|
|
109
|
+
connection = connect_sqlite(current_settings.database_url)
|
|
110
|
+
repository = SqliteTaskRepository(connection)
|
|
111
|
+
repository.initialize()
|
|
112
|
+
app.state.task_repository = repository
|
|
113
|
+
task_worker = build_worker(repository=repository, current_settings=current_settings)
|
|
114
|
+
replace_task_worker(app.state, task_worker)
|
|
115
|
+
app.state.db_connection = connection
|
|
116
|
+
app.state.settings_manager = settings_manager
|
|
117
|
+
recover_incomplete_tasks(repository, task_worker)
|
|
118
|
+
logger.info("application startup complete database=%s", current_settings.database_url)
|
|
119
|
+
try:
|
|
120
|
+
yield
|
|
121
|
+
finally:
|
|
122
|
+
task_worker = getattr(app.state, "task_worker", None)
|
|
123
|
+
if task_worker is not None:
|
|
124
|
+
task_worker.shutdown(wait=False)
|
|
125
|
+
logger.info("application shutdown")
|
|
126
|
+
connection.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
app = FastAPI(
|
|
130
|
+
title="BiliSum Service",
|
|
131
|
+
version=app_info.version,
|
|
132
|
+
description="Local-first backend service for BiliSum video summarization tasks.",
|
|
133
|
+
lifespan=lifespan,
|
|
134
|
+
)
|
|
135
|
+
app.mount("/static", StaticFiles(directory=WEB_STATIC_DIR), name="static")
|
|
136
|
+
app.mount("/media", StaticFiles(directory=CACHE_STATIC_DIR), name="media")
|
|
137
|
+
app.include_router(system_router)
|
|
138
|
+
app.include_router(videos_router)
|
|
139
|
+
app.include_router(tasks_router)
|
|
140
|
+
app.include_router(knowledge_router)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def frontend_shell_response() -> FileResponse:
|
|
144
|
+
return FileResponse(
|
|
145
|
+
WEB_STATIC_DIR / "index.html",
|
|
146
|
+
headers={
|
|
147
|
+
"Cache-Control": "no-store, no-cache, must-revalidate",
|
|
148
|
+
"Pragma": "no-cache",
|
|
149
|
+
"Expires": "0",
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.get("/", include_in_schema=False)
|
|
155
|
+
def index() -> FileResponse:
|
|
156
|
+
return frontend_shell_response()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.get("/videos/{video_id}", include_in_schema=False)
|
|
160
|
+
def video_detail_page(video_id: str) -> FileResponse:
|
|
161
|
+
del video_id
|
|
162
|
+
return frontend_shell_response()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.get("/settings", include_in_schema=False)
|
|
166
|
+
@app.get("/settings/{subpath:path}", include_in_schema=False)
|
|
167
|
+
def settings_page(subpath: str = "") -> FileResponse:
|
|
168
|
+
del subpath
|
|
169
|
+
return frontend_shell_response()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.get("/knowledge", include_in_schema=False)
|
|
173
|
+
@app.get("/knowledge/{subpath:path}", include_in_schema=False)
|
|
174
|
+
def knowledge_page(subpath: str = "") -> FileResponse:
|
|
175
|
+
del subpath
|
|
176
|
+
return frontend_shell_response()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.get("/health")
|
|
180
|
+
def health() -> dict[str, str]:
|
|
181
|
+
return {"status": "ok", "service": app_info.name, "version": app_info.version}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _run_command(command: list[str], runtime_channel: str, timeout: int = 3600):
|
|
185
|
+
return run_command(command, runtime_channel=runtime_channel, timeout=timeout)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _ensure_runtime_pip(python_executable, runtime_channel: str) -> None:
|
|
189
|
+
return ensure_runtime_pip(python_executable, runtime_channel)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _install_workspace_packages(python_executable, runtime_channel: str) -> None:
|
|
193
|
+
if is_frozen():
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
_ensure_runtime_pip(python_executable, runtime_channel)
|
|
197
|
+
root = repo_root()
|
|
198
|
+
_run_command(
|
|
199
|
+
[
|
|
200
|
+
str(python_executable),
|
|
201
|
+
"-m",
|
|
202
|
+
"pip",
|
|
203
|
+
"install",
|
|
204
|
+
"--upgrade",
|
|
205
|
+
"pip",
|
|
206
|
+
"setuptools",
|
|
207
|
+
"wheel",
|
|
208
|
+
"hatchling>=1.27.0",
|
|
209
|
+
],
|
|
210
|
+
runtime_channel=runtime_channel,
|
|
211
|
+
timeout=900,
|
|
212
|
+
)
|
|
213
|
+
command = [
|
|
214
|
+
str(python_executable),
|
|
215
|
+
"-m",
|
|
216
|
+
"pip",
|
|
217
|
+
"install",
|
|
218
|
+
"--no-build-isolation",
|
|
219
|
+
]
|
|
220
|
+
if runtime_channel != "base":
|
|
221
|
+
command.append("--no-deps")
|
|
222
|
+
command.extend(
|
|
223
|
+
[
|
|
224
|
+
str(root / "packages" / "infra"),
|
|
225
|
+
str(root / "packages" / "core"),
|
|
226
|
+
str(root / "apps" / "service"),
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
_run_command(command, runtime_channel=runtime_channel, timeout=1800)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def update_settings(payload: SettingsUpdatePayload) -> dict[str, object]:
|
|
233
|
+
previous_settings = settings_manager.current
|
|
234
|
+
current_settings = settings_manager.save(payload)
|
|
235
|
+
bootstrap_managed_runtime(current_settings.runtime_channel)
|
|
236
|
+
prepend_runtime_path(current_settings.runtime_channel)
|
|
237
|
+
activate_runtime_pythonpath(current_settings.runtime_channel)
|
|
238
|
+
current_settings.data_dir.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
current_settings.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
240
|
+
current_settings.tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
runtime_channel_changed = previous_settings.runtime_channel != current_settings.runtime_channel
|
|
242
|
+
if runtime_channel_changed:
|
|
243
|
+
clear_environment_probe_cache(previous_settings.runtime_channel)
|
|
244
|
+
clear_environment_probe_cache(current_settings.runtime_channel)
|
|
245
|
+
environment = detect_environment(current_settings.runtime_channel)
|
|
246
|
+
replace_task_worker(
|
|
247
|
+
app.state,
|
|
248
|
+
build_worker(
|
|
249
|
+
app.state.task_repository,
|
|
250
|
+
current_settings,
|
|
251
|
+
environment_info=environment,
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
return {
|
|
255
|
+
"saved": True,
|
|
256
|
+
"settings": serialize_settings(current_settings, environment_info=environment),
|
|
257
|
+
"message": "设置已保存。涉及服务监听地址的修改将在下次启动后生效。",
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def install_local_asr(reinstall: bool = False) -> dict[str, object]:
|
|
262
|
+
current_settings = settings_manager.current
|
|
263
|
+
runtime_channel = current_settings.runtime_channel
|
|
264
|
+
runtime_dir = ensure_runtime_channel(runtime_channel)
|
|
265
|
+
python_executable = runtime_python_executable(runtime_channel)
|
|
266
|
+
if runtime_dir is None or python_executable is None:
|
|
267
|
+
raise HTTPException(status_code=500, detail="Managed runtime is unavailable.")
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
_install_workspace_packages(python_executable, runtime_channel=runtime_channel)
|
|
271
|
+
_ensure_runtime_pip(python_executable, runtime_channel)
|
|
272
|
+
result = pip_install_with_fallbacks(
|
|
273
|
+
python_executable,
|
|
274
|
+
runtime_channel,
|
|
275
|
+
["faster-whisper>=1.1.1"],
|
|
276
|
+
reinstall=reinstall,
|
|
277
|
+
timeout=1800,
|
|
278
|
+
runner=_run_command,
|
|
279
|
+
)
|
|
280
|
+
except Exception as exc:
|
|
281
|
+
clear_environment_probe_cache(runtime_channel)
|
|
282
|
+
if isinstance(exc, HTTPException):
|
|
283
|
+
raise
|
|
284
|
+
detail = getattr(exc, "stderr", None) or getattr(exc, "stdout", None) or str(exc)
|
|
285
|
+
raise HTTPException(status_code=500, detail=str(detail)[-1500:]) from exc
|
|
286
|
+
|
|
287
|
+
clear_environment_probe_cache(runtime_channel)
|
|
288
|
+
environment = detect_environment(runtime_channel)
|
|
289
|
+
replace_task_worker(
|
|
290
|
+
app.state,
|
|
291
|
+
build_worker(
|
|
292
|
+
app.state.task_repository,
|
|
293
|
+
current_settings,
|
|
294
|
+
environment_info=environment,
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
write_runtime_metadata(
|
|
298
|
+
runtime_channel,
|
|
299
|
+
{
|
|
300
|
+
"runtimeChannel": runtime_channel,
|
|
301
|
+
"python": str(python_executable),
|
|
302
|
+
"localAsrInstalled": bool(environment.get("localAsrInstalled")),
|
|
303
|
+
"localAsrVersion": str(environment.get("localAsrVersion") or ""),
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
return {
|
|
307
|
+
"installed": bool(environment.get("localAsrInstalled")),
|
|
308
|
+
"runtimeChannel": runtime_channel,
|
|
309
|
+
"stdoutTail": ((getattr(result, "stdout", "") or "") + "\n" + (getattr(result, "stderr", "") or "")).strip()[-1500:],
|
|
310
|
+
"environment": environment,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def install_knowledge_dependencies(reinstall: bool = False) -> dict[str, object]:
|
|
315
|
+
current_settings = settings_manager.current
|
|
316
|
+
runtime_channel = current_settings.runtime_channel
|
|
317
|
+
use_current_python = _uses_current_service_python(runtime_channel)
|
|
318
|
+
if use_current_python:
|
|
319
|
+
runtime_dir = repo_root()
|
|
320
|
+
python_executable = Path(sys.executable).resolve()
|
|
321
|
+
runner = lambda command, runtime_channel, timeout=1800: _run_host_command(command, timeout=timeout)
|
|
322
|
+
else:
|
|
323
|
+
runtime_dir = ensure_runtime_channel(runtime_channel)
|
|
324
|
+
python_executable = runtime_python_executable(runtime_channel)
|
|
325
|
+
runner = _run_command
|
|
326
|
+
if runtime_dir is None or python_executable is None:
|
|
327
|
+
raise HTTPException(status_code=500, detail="Managed runtime is unavailable.")
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
if not use_current_python:
|
|
331
|
+
_install_workspace_packages(python_executable, runtime_channel=runtime_channel)
|
|
332
|
+
_ensure_runtime_pip(python_executable, runtime_channel)
|
|
333
|
+
result = pip_install_with_fallbacks(
|
|
334
|
+
python_executable,
|
|
335
|
+
runtime_channel,
|
|
336
|
+
["chromadb>=1.0.0", "sentence-transformers>=3.0"],
|
|
337
|
+
package_label="知识库依赖",
|
|
338
|
+
reinstall=reinstall,
|
|
339
|
+
timeout=1800,
|
|
340
|
+
runner=runner,
|
|
341
|
+
)
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
clear_environment_probe_cache(runtime_channel)
|
|
344
|
+
if isinstance(exc, HTTPException):
|
|
345
|
+
raise
|
|
346
|
+
detail = getattr(exc, "stderr", None) or getattr(exc, "stdout", None) or str(exc)
|
|
347
|
+
raise HTTPException(status_code=500, detail=str(detail)[-1500:]) from exc
|
|
348
|
+
|
|
349
|
+
importlib.invalidate_caches()
|
|
350
|
+
activate_runtime_pythonpath(runtime_channel)
|
|
351
|
+
clear_environment_probe_cache(runtime_channel)
|
|
352
|
+
environment = detect_environment(runtime_channel)
|
|
353
|
+
replace_task_worker(
|
|
354
|
+
app.state,
|
|
355
|
+
build_worker(
|
|
356
|
+
app.state.task_repository,
|
|
357
|
+
current_settings,
|
|
358
|
+
environment_info=environment,
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
write_runtime_metadata(
|
|
362
|
+
runtime_channel,
|
|
363
|
+
{
|
|
364
|
+
"runtimeChannel": runtime_channel,
|
|
365
|
+
"python": str(python_executable),
|
|
366
|
+
"chromadbInstalled": bool(environment.get("chromadbInstalled")),
|
|
367
|
+
"chromadbVersion": str(environment.get("chromadbVersion") or ""),
|
|
368
|
+
"sentenceTransformersInstalled": bool(environment.get("sentenceTransformersInstalled")),
|
|
369
|
+
"sentenceTransformersVersion": str(environment.get("sentenceTransformersVersion") or ""),
|
|
370
|
+
"knowledgeDependenciesReady": bool(environment.get("knowledgeDependenciesReady")),
|
|
371
|
+
},
|
|
372
|
+
)
|
|
373
|
+
return {
|
|
374
|
+
"installed": bool(environment.get("knowledgeDependenciesReady")),
|
|
375
|
+
"runtimeChannel": runtime_channel,
|
|
376
|
+
"stdoutTail": ((getattr(result, "stdout", "") or "") + "\n" + (getattr(result, "stderr", "") or "")).strip()[-1500:],
|
|
377
|
+
"environment": environment,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def get_task_mindmap(task_id: str) -> TaskMindMapResponse:
|
|
382
|
+
task_store: SqliteTaskRepository = app.state.task_repository
|
|
383
|
+
record = task_store.get_task(task_id)
|
|
384
|
+
if record is None:
|
|
385
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
386
|
+
|
|
387
|
+
result = record.result
|
|
388
|
+
if result is None:
|
|
389
|
+
return TaskMindMapResponse(task_id=task_id, status="idle")
|
|
390
|
+
|
|
391
|
+
mindmap_path = result.mindmap_artifact_path or result.artifacts.get("mindmap_path")
|
|
392
|
+
status_value = result.mindmap_status or ("ready" if mindmap_path else "idle")
|
|
393
|
+
error_message = result.mindmap_error_message
|
|
394
|
+
if status_value == "generating":
|
|
395
|
+
return TaskMindMapResponse(
|
|
396
|
+
task_id=task_id,
|
|
397
|
+
status="generating",
|
|
398
|
+
error_message=error_message,
|
|
399
|
+
updated_at=result.mindmap_updated_at,
|
|
400
|
+
mindmap=None,
|
|
401
|
+
)
|
|
402
|
+
if status_value == "failed":
|
|
403
|
+
return TaskMindMapResponse(
|
|
404
|
+
task_id=task_id,
|
|
405
|
+
status="failed",
|
|
406
|
+
error_message=error_message,
|
|
407
|
+
updated_at=result.mindmap_updated_at,
|
|
408
|
+
mindmap=None,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
mindmap = None
|
|
412
|
+
if mindmap_path:
|
|
413
|
+
try:
|
|
414
|
+
mindmap = load_task_mindmap(mindmap_path)
|
|
415
|
+
except HTTPException:
|
|
416
|
+
status_value = "failed"
|
|
417
|
+
error_message = "思维导图文件缺失或已损坏,请重新生成。"
|
|
418
|
+
if mindmap is not None:
|
|
419
|
+
status_value = "ready"
|
|
420
|
+
|
|
421
|
+
return TaskMindMapResponse(
|
|
422
|
+
task_id=task_id,
|
|
423
|
+
status=status_value,
|
|
424
|
+
error_message=error_message,
|
|
425
|
+
updated_at=result.mindmap_updated_at,
|
|
426
|
+
mindmap=mindmap,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def generate_task_mindmap(task_id: str, force: bool = False) -> TaskMindMapResponse:
|
|
431
|
+
task_store: SqliteTaskRepository = app.state.task_repository
|
|
432
|
+
task_worker = app.state.task_worker
|
|
433
|
+
record = task_store.get_task(task_id)
|
|
434
|
+
if record is None:
|
|
435
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
436
|
+
if record.status != TaskStatus.COMPLETED or record.result is None:
|
|
437
|
+
raise HTTPException(status_code=400, detail="仅已完成且有结果的任务可以生成思维导图。")
|
|
438
|
+
if not record.result.knowledge_note_markdown.strip():
|
|
439
|
+
raise HTTPException(status_code=400, detail="当前任务缺少知识笔记,暂时无法生成思维导图。")
|
|
440
|
+
if not record.result.artifacts.get("summary_path"):
|
|
441
|
+
raise HTTPException(status_code=400, detail="当前任务缺少摘要文件,暂时无法生成思维导图。")
|
|
442
|
+
|
|
443
|
+
existing_path = record.result.mindmap_artifact_path or record.result.artifacts.get("mindmap_path")
|
|
444
|
+
if record.result.mindmap_status == "generating" and not force:
|
|
445
|
+
return TaskMindMapResponse(
|
|
446
|
+
task_id=task_id,
|
|
447
|
+
status="generating",
|
|
448
|
+
error_message=None,
|
|
449
|
+
updated_at=record.result.mindmap_updated_at,
|
|
450
|
+
mindmap=None,
|
|
451
|
+
)
|
|
452
|
+
if existing_path and record.result.mindmap_status == "ready" and not force:
|
|
453
|
+
try:
|
|
454
|
+
return TaskMindMapResponse(
|
|
455
|
+
task_id=task_id,
|
|
456
|
+
status="ready",
|
|
457
|
+
error_message=None,
|
|
458
|
+
updated_at=record.result.mindmap_updated_at,
|
|
459
|
+
mindmap=load_task_mindmap(existing_path),
|
|
460
|
+
)
|
|
461
|
+
except HTTPException:
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
generating_result = record.result.model_copy(
|
|
465
|
+
update={
|
|
466
|
+
"mindmap_status": "generating",
|
|
467
|
+
"mindmap_error_message": None,
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
task_store.save_result(task_id, generating_result)
|
|
471
|
+
task_worker.submit_mindmap(task_id, force=force)
|
|
472
|
+
refreshed = task_store.get_task(task_id)
|
|
473
|
+
refreshed_result = refreshed.result if refreshed is not None else generating_result
|
|
474
|
+
return TaskMindMapResponse(
|
|
475
|
+
task_id=task_id,
|
|
476
|
+
status=refreshed_result.mindmap_status,
|
|
477
|
+
error_message=refreshed_result.mindmap_error_message,
|
|
478
|
+
updated_at=refreshed_result.mindmap_updated_at,
|
|
479
|
+
mindmap=None,
|
|
480
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from video_sum_infra.app import AppInfo
|
|
4
|
+
from video_sum_infra.config import ServiceSettings
|
|
5
|
+
from video_sum_infra.runtime import log_dir, web_static_dir
|
|
6
|
+
|
|
7
|
+
from video_sum_service.settings_manager import SettingsManager
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("video_sum_service.app")
|
|
10
|
+
|
|
11
|
+
settings_manager = SettingsManager(ServiceSettings())
|
|
12
|
+
settings = settings_manager.load()
|
|
13
|
+
app_info = AppInfo.load()
|
|
14
|
+
|
|
15
|
+
WEB_STATIC_DIR = web_static_dir()
|
|
16
|
+
CACHE_STATIC_DIR = settings.cache_dir
|
|
17
|
+
COVER_CACHE_DIR = CACHE_STATIC_DIR / "covers"
|
|
18
|
+
LOCAL_MEDIA_UPLOAD_DIR = CACHE_STATIC_DIR / "uploads"
|
|
19
|
+
|
|
20
|
+
settings.data_dir.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
settings.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
settings.tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
LOCAL_MEDIA_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
log_dir().mkdir(parents=True, exist_ok=True)
|