bilisum 1.13.2 → 1.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +4 -4
  2. package/bin/bilisum.js +157 -76
  3. package/package.json +7 -2
  4. package/runtime/VERSION +1 -0
  5. package/runtime/apps/service/pyproject.toml +29 -0
  6. package/runtime/apps/service/src/video_sum_service/__init__.py +1 -0
  7. package/runtime/apps/service/src/video_sum_service/__main__.py +5 -0
  8. package/runtime/apps/service/src/video_sum_service/app.py +480 -0
  9. package/runtime/apps/service/src/video_sum_service/context.py +24 -0
  10. package/runtime/apps/service/src/video_sum_service/integrations.py +264 -0
  11. package/runtime/apps/service/src/video_sum_service/knowledge/__init__.py +5 -0
  12. package/runtime/apps/service/src/video_sum_service/knowledge/index_service.py +408 -0
  13. package/runtime/apps/service/src/video_sum_service/knowledge/local_llm.py +319 -0
  14. package/runtime/apps/service/src/video_sum_service/knowledge/rag_service.py +492 -0
  15. package/runtime/apps/service/src/video_sum_service/knowledge/tag_service.py +242 -0
  16. package/runtime/apps/service/src/video_sum_service/main.py +31 -0
  17. package/runtime/apps/service/src/video_sum_service/repository.py +942 -0
  18. package/runtime/apps/service/src/video_sum_service/routers/__init__.py +1 -0
  19. package/runtime/apps/service/src/video_sum_service/routers/knowledge.py +272 -0
  20. package/runtime/apps/service/src/video_sum_service/routers/system.py +280 -0
  21. package/runtime/apps/service/src/video_sum_service/routers/tasks.py +287 -0
  22. package/runtime/apps/service/src/video_sum_service/routers/videos.py +766 -0
  23. package/runtime/apps/service/src/video_sum_service/runtime_support.py +1007 -0
  24. package/runtime/apps/service/src/video_sum_service/schemas.py +418 -0
  25. package/runtime/apps/service/src/video_sum_service/settings_manager.py +130 -0
  26. package/runtime/apps/service/src/video_sum_service/task_artifacts.py +102 -0
  27. package/runtime/apps/service/src/video_sum_service/task_exports.py +108 -0
  28. package/runtime/apps/service/src/video_sum_service/transcribe_worker.py +9 -0
  29. package/runtime/apps/service/src/video_sum_service/video_assets.py +623 -0
  30. package/runtime/apps/service/src/video_sum_service/worker.py +381 -0
  31. package/runtime/apps/web/static/apple-touch-icon.png +0 -0
  32. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  33. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  34. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  35. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  36. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  37. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  38. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  39. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  40. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  41. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  42. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  43. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  44. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  45. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  46. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  47. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  48. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  49. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  50. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  51. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  52. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  53. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  54. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  55. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  56. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  57. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  58. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  59. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  60. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  61. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  62. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  63. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  64. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  65. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  66. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  67. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  68. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  69. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  70. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  71. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  72. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  73. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  74. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  75. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  76. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  77. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  78. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  79. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  80. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  81. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  82. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  83. package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  84. package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  85. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  86. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  87. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  88. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  89. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  90. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  91. package/runtime/apps/web/static/assets/icons/icon-180.png +0 -0
  92. package/runtime/apps/web/static/assets/icons/icon-512.png +0 -0
  93. package/runtime/apps/web/static/assets/icons/icon.svg +17 -0
  94. package/runtime/apps/web/static/assets/index-Cj5PVjQg.js +388 -0
  95. package/runtime/apps/web/static/assets/index-DU2t4_7s.css +1 -0
  96. package/runtime/apps/web/static/favicon-32x32.png +0 -0
  97. package/runtime/apps/web/static/favicon.ico +0 -0
  98. package/runtime/apps/web/static/favicon.svg +17 -0
  99. package/runtime/apps/web/static/index.html +19 -0
  100. package/runtime/apps/web/static/js/api.js +123 -0
  101. package/runtime/apps/web/static/js/main.js +932 -0
  102. package/runtime/apps/web/static/js/state.js +28 -0
  103. package/runtime/apps/web/static/js/utils.js +81 -0
  104. package/runtime/apps/web/static/js/views/home.js +604 -0
  105. package/runtime/apps/web/static/js/views/settings.js +477 -0
  106. package/runtime/apps/web/static/styles.css +2206 -0
  107. package/runtime/packages/core/pyproject.toml +18 -0
  108. package/runtime/packages/core/src/video_sum_core/__init__.py +1 -0
  109. package/runtime/packages/core/src/video_sum_core/errors.py +22 -0
  110. package/runtime/packages/core/src/video_sum_core/markdown_exports.py +154 -0
  111. package/runtime/packages/core/src/video_sum_core/models/__init__.py +1 -0
  112. package/runtime/packages/core/src/video_sum_core/models/tasks.py +70 -0
  113. package/runtime/packages/core/src/video_sum_core/pipeline/__init__.py +1 -0
  114. package/runtime/packages/core/src/video_sum_core/pipeline/base.py +46 -0
  115. package/runtime/packages/core/src/video_sum_core/pipeline/real.py +3467 -0
  116. package/runtime/packages/core/src/video_sum_core/transcribe_subprocess.py +173 -0
  117. package/runtime/packages/core/src/video_sum_core/utils.py +127 -0
  118. package/runtime/packages/infra/pyproject.toml +16 -0
  119. package/runtime/packages/infra/src/video_sum_infra/__init__.py +1 -0
  120. package/runtime/packages/infra/src/video_sum_infra/app.py +39 -0
  121. package/runtime/packages/infra/src/video_sum_infra/config.py +393 -0
  122. package/runtime/packages/infra/src/video_sum_infra/db.py +34 -0
  123. package/runtime/packages/infra/src/video_sum_infra/logging.py +54 -0
  124. package/runtime/packages/infra/src/video_sum_infra/paths.py +6 -0
  125. package/runtime/packages/infra/src/video_sum_infra/runtime.py +485 -0
@@ -0,0 +1,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)