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,1007 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ import textwrap
7
+ import venv
8
+ import importlib
9
+ from pathlib import Path
10
+
11
+ from fastapi import HTTPException
12
+
13
+ from video_sum_core.pipeline.real import PipelineSettings, RealPipelineRunner
14
+ from video_sum_infra.config import ServiceSettings
15
+ from video_sum_infra.runtime import (
16
+ activate_runtime_pythonpath,
17
+ bootstrap_managed_runtime,
18
+ ffmpeg_location,
19
+ is_frozen,
20
+ managed_runtime_root,
21
+ managed_runtime_dir,
22
+ prepend_runtime_path,
23
+ read_runtime_metadata,
24
+ repo_root,
25
+ runtime_library_dirs,
26
+ runtime_python_candidates,
27
+ runtime_python_executable,
28
+ sanitized_subprocess_dll_search,
29
+ write_runtime_metadata,
30
+ )
31
+
32
+ from video_sum_service.context import logger, settings_manager
33
+ from video_sum_service.repository import SqliteTaskRepository
34
+ from video_sum_service.settings_manager import SettingsUpdatePayload
35
+ from video_sum_service.worker import TaskWorker
36
+
37
+ _environment_probe_cache: dict[str, dict[str, object]] = {}
38
+ _environment_probe_failures: dict[str, str] = {}
39
+ _PIP_INDEX_CANDIDATES: tuple[tuple[str, str | None], ...] = (
40
+ ("official", None),
41
+ ("tsinghua", "https://pypi.tuna.tsinghua.edu.cn/simple"),
42
+ ("aliyun", "https://mirrors.aliyun.com/pypi/simple"),
43
+ )
44
+ _KNOWN_RUNTIME_CHANNELS: tuple[str, ...] = ("base", "gpu-cu128", "gpu-cu126", "gpu-cu124")
45
+ _RUNTIME_EXTENSION_PACKAGE_KEYS: set[str] = {
46
+ "torch",
47
+ "torchvision",
48
+ "torchaudio",
49
+ "nvidia",
50
+ "triton",
51
+ "faster_whisper",
52
+ "ctranslate2",
53
+ "onnxruntime",
54
+ "tokenizers",
55
+ "huggingface_hub",
56
+ "hf_xet",
57
+ "chromadb",
58
+ "sentence_transformers",
59
+ }
60
+
61
+
62
+ def _split_env_urls(raw_value: str | None) -> list[str]:
63
+ if not raw_value:
64
+ return []
65
+ normalized = raw_value.replace("\r", "\n").replace(";", "\n").replace(",", "\n")
66
+ return [item.strip() for item in normalized.splitlines() if item.strip()]
67
+
68
+
69
+ def _torch_index_candidates(cuda_variant: str) -> list[tuple[str, str]]:
70
+ candidates: list[tuple[str, str]] = [("official", f"https://download.pytorch.org/whl/{cuda_variant}")]
71
+ for index, url in enumerate(_split_env_urls(os.environ.get("VIDEO_SUM_TORCH_INDEX_URLS")), start=1):
72
+ candidates.append((f"custom-{index}", url))
73
+ return candidates
74
+
75
+
76
+ def windows_hidden_subprocess_kwargs() -> dict[str, object]:
77
+ if os.name != "nt":
78
+ return {}
79
+
80
+ kwargs: dict[str, object] = {}
81
+ creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
82
+ if creationflags:
83
+ kwargs["creationflags"] = creationflags
84
+
85
+ startupinfo_cls = getattr(subprocess, "STARTUPINFO", None)
86
+ use_show_window = getattr(subprocess, "STARTF_USESHOWWINDOW", 0)
87
+ sw_hide = getattr(subprocess, "SW_HIDE", 0)
88
+ if startupinfo_cls is not None:
89
+ startupinfo = startupinfo_cls()
90
+ startupinfo.dwFlags |= use_show_window
91
+ startupinfo.wShowWindow = sw_hide
92
+ kwargs["startupinfo"] = startupinfo
93
+
94
+ return kwargs
95
+
96
+
97
+ def runtime_subprocess_env(runtime_channel: str) -> dict[str, str]:
98
+ env = dict(os.environ)
99
+ for key in ("PYTHONHOME", "PYTHONPATH", "PYTHONEXECUTABLE", "__PYVENV_LAUNCHER__"):
100
+ env.pop(key, None)
101
+ env["PYTHONIOENCODING"] = "utf-8"
102
+ env["PYTHONUTF8"] = "1"
103
+
104
+ path_entries = [str(path) for path in runtime_library_dirs(runtime_channel)]
105
+ ffmpeg_dir = ffmpeg_location()
106
+ if ffmpeg_dir is not None:
107
+ path_entries.append(str(ffmpeg_dir))
108
+
109
+ current_path = env.get("PATH", "")
110
+ inherited_entries: list[str] = []
111
+ blocked_prefixes: list[Path] = []
112
+ if is_frozen():
113
+ blocked_prefixes.append(Path(sys.executable).resolve().parent)
114
+ meipass = getattr(sys, "_MEIPASS", "")
115
+ if meipass:
116
+ blocked_prefixes.append(Path(meipass).resolve())
117
+
118
+ for raw_entry in current_path.split(os.pathsep):
119
+ entry = raw_entry.strip()
120
+ if not entry:
121
+ continue
122
+ try:
123
+ entry_path = Path(entry).resolve()
124
+ except OSError:
125
+ inherited_entries.append(entry)
126
+ continue
127
+ if any(str(entry_path).lower().startswith(str(prefix).lower()) for prefix in blocked_prefixes):
128
+ continue
129
+ inherited_entries.append(entry)
130
+
131
+ merged: list[str] = []
132
+ for entry in [*path_entries, *inherited_entries]:
133
+ if entry and entry not in merged:
134
+ merged.append(entry)
135
+ env["PATH"] = os.pathsep.join(merged)
136
+ return env
137
+
138
+
139
+ def run_command(command: list[str], runtime_channel: str, timeout: int = 3600) -> subprocess.CompletedProcess[str]:
140
+ with sanitized_subprocess_dll_search():
141
+ return subprocess.run(
142
+ command,
143
+ capture_output=True,
144
+ text=True,
145
+ encoding="utf-8",
146
+ errors="replace",
147
+ timeout=timeout,
148
+ check=True,
149
+ env=runtime_subprocess_env(runtime_channel),
150
+ cwd=managed_runtime_dir(runtime_channel),
151
+ **windows_hidden_subprocess_kwargs(),
152
+ )
153
+
154
+
155
+ def run_host_command(command: list[str], timeout: int = 3600) -> subprocess.CompletedProcess[str]:
156
+ env = dict(os.environ)
157
+ for key in ("PYTHONHOME", "PYTHONPATH", "PYTHONEXECUTABLE", "__PYVENV_LAUNCHER__"):
158
+ env.pop(key, None)
159
+ env["PYTHONIOENCODING"] = "utf-8"
160
+ env["PYTHONUTF8"] = "1"
161
+ with sanitized_subprocess_dll_search():
162
+ return subprocess.run(
163
+ command,
164
+ capture_output=True,
165
+ text=True,
166
+ encoding="utf-8",
167
+ errors="replace",
168
+ timeout=timeout,
169
+ check=True,
170
+ env=env,
171
+ cwd=repo_root(),
172
+ **windows_hidden_subprocess_kwargs(),
173
+ )
174
+
175
+
176
+ def uses_current_service_python(runtime_channel: str) -> bool:
177
+ return not is_frozen() and runtime_channel == "base"
178
+
179
+
180
+ def command_error_detail(exc: subprocess.CalledProcessError, fallback: str) -> str:
181
+ parts = [str(exc.stdout or "").strip(), str(exc.stderr or "").strip()]
182
+ merged = "\n".join(part for part in parts if part).strip()
183
+ if not merged:
184
+ merged = str(exc)
185
+ merged = merged[-1500:]
186
+ return f"{fallback}\n\n{merged}".strip()
187
+
188
+
189
+ def pip_install_error_detail(
190
+ package_label: str,
191
+ attempts: list[tuple[str, subprocess.CalledProcessError]],
192
+ ) -> str:
193
+ if not attempts:
194
+ return f"安装 {package_label} 失败。"
195
+
196
+ last_label, last_exc = attempts[-1]
197
+ combined = "\n".join(
198
+ (str(exc.stdout or "").strip() + "\n" + str(exc.stderr or "").strip()).strip()
199
+ for _, exc in attempts
200
+ )
201
+ normalized = combined.lower()
202
+ hints = [
203
+ f"安装 {package_label} 失败。已尝试官方 PyPI 与国内镜像(清华、阿里云)。",
204
+ ]
205
+ if "ssl" in normalized or "unexpected_eof_while_reading" in normalized:
206
+ hints.append("检测到 SSL 握手异常,通常是当前网络到包索引的连接不稳定或被代理拦截。")
207
+ if "no matching distribution found" in normalized and "from versions: none" in normalized:
208
+ hints.append("当前输出更像是索引访问失败,而不是包本身不存在。")
209
+
210
+ last_detail = command_error_detail(last_exc, f"最后一次尝试使用 {last_label} 源仍然失败。")
211
+ return "\n\n".join([*hints, last_detail])
212
+
213
+
214
+ def pip_install_with_fallbacks(
215
+ python_executable: Path,
216
+ runtime_channel: str,
217
+ packages: list[str],
218
+ *,
219
+ package_label: str = "本地 ASR 依赖",
220
+ reinstall: bool = False,
221
+ timeout: int = 1800,
222
+ runner=run_command,
223
+ ) -> subprocess.CompletedProcess[str]:
224
+ attempts: list[tuple[str, subprocess.CalledProcessError]] = []
225
+
226
+ for label, index_url in _PIP_INDEX_CANDIDATES:
227
+ command = [
228
+ str(python_executable),
229
+ "-m",
230
+ "pip",
231
+ "install",
232
+ "--disable-pip-version-check",
233
+ "--upgrade",
234
+ "--upgrade-strategy",
235
+ "only-if-needed",
236
+ ]
237
+ if reinstall:
238
+ command.append("--force-reinstall")
239
+ if index_url:
240
+ command.extend(["--index-url", index_url])
241
+ command.extend(packages)
242
+
243
+ try:
244
+ return runner(command, runtime_channel=runtime_channel, timeout=timeout)
245
+ except subprocess.CalledProcessError as exc:
246
+ attempts.append((label, exc))
247
+
248
+ raise HTTPException(status_code=500, detail=pip_install_error_detail(package_label, attempts))
249
+
250
+
251
+ def pip_index_options() -> list[dict[str, str]]:
252
+ return [
253
+ {"label": label, "url": index_url or "https://pypi.org/simple"}
254
+ for label, index_url in _PIP_INDEX_CANDIDATES
255
+ ]
256
+
257
+
258
+ def torch_install_with_fallbacks(
259
+ python_executable: Path,
260
+ runtime_channel: str,
261
+ cuda_variant: str,
262
+ *,
263
+ timeout: int = 1800,
264
+ runner=run_command,
265
+ ) -> subprocess.CompletedProcess[str]:
266
+ attempts: list[tuple[str, subprocess.CalledProcessError]] = []
267
+
268
+ for label, index_url in _torch_index_candidates(cuda_variant):
269
+ command = [
270
+ str(python_executable),
271
+ "-m",
272
+ "pip",
273
+ "install",
274
+ "--disable-pip-version-check",
275
+ "--upgrade",
276
+ "torch",
277
+ "torchvision",
278
+ "torchaudio",
279
+ "--index-url",
280
+ index_url,
281
+ ]
282
+ try:
283
+ return runner(command, runtime_channel=runtime_channel, timeout=timeout)
284
+ except subprocess.CalledProcessError as exc:
285
+ attempts.append((label, exc))
286
+
287
+ raise HTTPException(status_code=500, detail=pip_install_error_detail("CUDA 运行时依赖", attempts))
288
+
289
+
290
+ def ensure_runtime_pip(python_executable: Path, runtime_channel: str) -> None:
291
+ try:
292
+ run_command([str(python_executable), "-m", "pip", "--version"], runtime_channel=runtime_channel, timeout=120)
293
+ return
294
+ except subprocess.CalledProcessError as exc:
295
+ detail = (exc.stderr or exc.stdout or str(exc)).strip()
296
+ if "No module named pip" not in detail:
297
+ raise
298
+
299
+ run_command([str(python_executable), "-m", "ensurepip", "--upgrade"], runtime_channel=runtime_channel, timeout=300)
300
+
301
+
302
+ def install_workspace_packages(python_executable: Path, runtime_channel: str) -> None:
303
+ if is_frozen():
304
+ return
305
+
306
+ ensure_runtime_pip(python_executable, runtime_channel)
307
+ root = repo_root()
308
+ run_command(
309
+ [
310
+ str(python_executable),
311
+ "-m",
312
+ "pip",
313
+ "install",
314
+ "--upgrade",
315
+ "pip",
316
+ "setuptools",
317
+ "wheel",
318
+ "hatchling>=1.27.0",
319
+ ],
320
+ runtime_channel=runtime_channel,
321
+ timeout=900,
322
+ )
323
+ command = [
324
+ str(python_executable),
325
+ "-m",
326
+ "pip",
327
+ "install",
328
+ "--no-build-isolation",
329
+ ]
330
+ if runtime_channel != "base":
331
+ command.append("--no-deps")
332
+ command.extend(
333
+ [
334
+ str(root / "packages" / "infra"),
335
+ str(root / "packages" / "core"),
336
+ str(root / "apps" / "service"),
337
+ ]
338
+ )
339
+ run_command(command, runtime_channel=runtime_channel, timeout=1800)
340
+
341
+
342
+ def create_source_runtime(runtime_channel: str) -> Path:
343
+ runtime_dir = managed_runtime_dir(runtime_channel)
344
+ venv.EnvBuilder(with_pip=True, clear=True).create(runtime_dir)
345
+ python_executable = next((candidate for candidate in runtime_python_candidates(runtime_dir) if candidate.exists()), None)
346
+ if python_executable is None:
347
+ raise HTTPException(status_code=500, detail="Managed runtime creation failed: python.exe missing.")
348
+ install_workspace_packages(python_executable, runtime_channel=runtime_channel)
349
+ return runtime_dir
350
+
351
+
352
+ def ensure_runtime_channel(runtime_channel: str) -> Path | None:
353
+ if runtime_channel == "base":
354
+ bootstrap_managed_runtime("base")
355
+ python_executable = runtime_python_executable("base")
356
+ if python_executable is not None:
357
+ return managed_runtime_dir("base")
358
+ if not is_frozen():
359
+ return create_source_runtime("base")
360
+ raise HTTPException(status_code=500, detail="Bundled base runtime is missing.")
361
+
362
+ target_dir = managed_runtime_dir(runtime_channel)
363
+ base_dir = ensure_runtime_channel("base")
364
+ if base_dir is None or not base_dir.exists():
365
+ raise HTTPException(status_code=500, detail="Base runtime is unavailable.")
366
+
367
+ base_metadata = read_runtime_metadata(base_dir)
368
+ target_metadata = read_runtime_metadata(target_dir)
369
+ target_ready = runtime_python_executable(runtime_channel) is not None
370
+ target_matches_base = (
371
+ target_metadata.get("appVersion") == base_metadata.get("appVersion")
372
+ and target_metadata.get("runtimeLayout") == base_metadata.get("runtimeLayout")
373
+ )
374
+ if target_ready and target_matches_base:
375
+ return target_dir
376
+
377
+ if target_ready and target_dir.exists():
378
+ sync_runtime_base(target_dir, base_dir, runtime_channel)
379
+ return target_dir
380
+
381
+ if target_dir.exists():
382
+ shutil.rmtree(target_dir)
383
+
384
+ shutil.copytree(base_dir, target_dir, dirs_exist_ok=True)
385
+ return target_dir
386
+
387
+
388
+ def inspect_runtime_channels() -> dict[str, object]:
389
+ root = managed_runtime_root()
390
+ discovered = set(_KNOWN_RUNTIME_CHANNELS)
391
+ if root.exists():
392
+ discovered.update(item.name for item in root.iterdir() if item.is_dir())
393
+
394
+ base_dir = managed_runtime_dir("base")
395
+ base_metadata = read_runtime_metadata(base_dir)
396
+ base_app_version = str(base_metadata.get("appVersion") or "")
397
+ base_layout = str(base_metadata.get("runtimeLayout") or "")
398
+ channels: list[dict[str, object]] = []
399
+
400
+ for runtime_channel in sorted(discovered, key=lambda item: (item != "base", item)):
401
+ runtime_dir = managed_runtime_dir(runtime_channel)
402
+ metadata = read_runtime_metadata(runtime_dir)
403
+ python_executable = runtime_python_executable(runtime_channel)
404
+ exists = runtime_dir.exists()
405
+ ready = python_executable is not None
406
+ app_version = str(metadata.get("appVersion") or "")
407
+ layout = str(metadata.get("runtimeLayout") or "")
408
+ needs_update = bool(
409
+ exists
410
+ and runtime_channel != "base"
411
+ and ready
412
+ and (
413
+ (base_app_version and app_version != base_app_version)
414
+ or (base_layout and layout != base_layout)
415
+ )
416
+ )
417
+ channels.append(
418
+ {
419
+ "runtimeChannel": runtime_channel,
420
+ "path": str(runtime_dir),
421
+ "exists": exists,
422
+ "ready": ready,
423
+ "python": str(python_executable or ""),
424
+ "appVersion": app_version,
425
+ "runtimeLayout": layout,
426
+ "targetAppVersion": base_app_version,
427
+ "targetRuntimeLayout": base_layout,
428
+ "needsUpdate": needs_update,
429
+ "cudaVariant": str(metadata.get("cudaVariant") or ""),
430
+ "localAsrInstalled": bool(metadata.get("localAsrInstalled")),
431
+ "knowledgeDependenciesReady": bool(metadata.get("knowledgeDependenciesReady")),
432
+ }
433
+ )
434
+
435
+ return {
436
+ "baseAppVersion": base_app_version,
437
+ "baseRuntimeLayout": base_layout,
438
+ "pipIndexes": pip_index_options(),
439
+ "channels": channels,
440
+ }
441
+
442
+
443
+ def sync_runtime_channel(runtime_channel: str) -> dict[str, object]:
444
+ if runtime_channel == "base":
445
+ runtime_dir = ensure_runtime_channel("base")
446
+ else:
447
+ runtime_dir = ensure_runtime_channel(runtime_channel)
448
+ clear_environment_probe_cache(runtime_channel)
449
+ environment = detect_environment(runtime_channel)
450
+ return {
451
+ "synced": runtime_dir is not None,
452
+ "runtimeChannel": runtime_channel,
453
+ "path": str(runtime_dir or ""),
454
+ "environment": environment,
455
+ }
456
+
457
+
458
+ def sync_all_runtime_channels() -> dict[str, object]:
459
+ status = inspect_runtime_channels()
460
+ channels = [
461
+ str(channel["runtimeChannel"])
462
+ for channel in status["channels"]
463
+ if channel.get("exists") and channel.get("runtimeChannel") != "base"
464
+ and (channel.get("needsUpdate") or not channel.get("ready"))
465
+ ]
466
+ results = [sync_runtime_channel(runtime_channel) for runtime_channel in channels]
467
+ return {
468
+ "synced": True,
469
+ "channels": results,
470
+ "runtimeStatus": inspect_runtime_channels(),
471
+ }
472
+
473
+
474
+ def sync_runtime_base(target_dir: Path, base_dir: Path, runtime_channel: str) -> None:
475
+ target_metadata = read_runtime_metadata(target_dir)
476
+ copy_runtime_item(base_dir / "stdlib", target_dir / "stdlib")
477
+ copy_runtime_item(base_dir / "DLLs", target_dir / "DLLs")
478
+ sync_runtime_lib(target_dir / "Lib", base_dir / "Lib")
479
+ sync_runtime_scripts(target_dir / "Scripts", base_dir / "Scripts")
480
+
481
+ base_metadata = read_runtime_metadata(base_dir)
482
+ (target_dir / "video_sum_runtime.json").write_text(
483
+ json.dumps(
484
+ {
485
+ **target_metadata,
486
+ "runtimeChannel": runtime_channel,
487
+ "runtimeLayout": base_metadata.get("runtimeLayout"),
488
+ "appVersion": base_metadata.get("appVersion"),
489
+ "pythonVersion": base_metadata.get("pythonVersion"),
490
+ },
491
+ ensure_ascii=False,
492
+ indent=2,
493
+ ),
494
+ encoding="utf-8",
495
+ )
496
+
497
+
498
+ def sync_runtime_scripts(target_scripts_dir: Path, base_scripts_dir: Path) -> None:
499
+ if not base_scripts_dir.exists():
500
+ return
501
+ target_scripts_dir.mkdir(parents=True, exist_ok=True)
502
+ for pattern in ("video-sum-service*", "video-sum-transcribe-worker*"):
503
+ for item in base_scripts_dir.glob(pattern):
504
+ copy_runtime_item(item, target_scripts_dir / item.name)
505
+
506
+
507
+ def sync_runtime_lib(target_lib_dir: Path, base_lib_dir: Path) -> None:
508
+ if not base_lib_dir.exists():
509
+ return
510
+ target_lib_dir.mkdir(parents=True, exist_ok=True)
511
+ for item in base_lib_dir.iterdir():
512
+ if item.name == "site-packages":
513
+ sync_runtime_site_packages(target_lib_dir / "site-packages", item)
514
+ continue
515
+ copy_runtime_item(item, target_lib_dir / item.name)
516
+
517
+
518
+ def sync_runtime_site_packages(
519
+ target_site_packages: Path,
520
+ base_site_packages: Path,
521
+ ) -> None:
522
+ target_site_packages.mkdir(parents=True, exist_ok=True)
523
+ for item in base_site_packages.iterdir():
524
+ if runtime_site_package_item_protected(item):
525
+ continue
526
+ remove_matching_dist_info(target_site_packages, item)
527
+ copy_runtime_item(item, target_site_packages / item.name)
528
+
529
+
530
+ def runtime_site_package_item_protected(item: Path) -> bool:
531
+ package_key = runtime_site_package_key(item)
532
+ return package_key in _RUNTIME_EXTENSION_PACKAGE_KEYS or package_key.startswith("nvidia_")
533
+
534
+
535
+ def runtime_site_package_key(item: Path) -> str:
536
+ name = item.name
537
+ for suffix in (".dist-info", ".egg-info"):
538
+ if name.endswith(suffix):
539
+ name = name[: -len(suffix)]
540
+ break
541
+ if "-" in name:
542
+ parts = name.split("-")
543
+ version_index = next((index for index, part in enumerate(parts) if part[:1].isdigit()), len(parts))
544
+ name = "-".join(parts[:version_index]) or parts[0]
545
+ return name.replace("-", "_").lower()
546
+
547
+
548
+ def remove_matching_dist_info(target_site_packages: Path, source: Path) -> None:
549
+ if not (source.name.endswith(".dist-info") or source.name.endswith(".egg-info")):
550
+ return
551
+ source_key = runtime_site_package_key(source)
552
+ for target in list(target_site_packages.glob("*.dist-info")) + list(target_site_packages.glob("*.egg-info")):
553
+ if runtime_site_package_key(target) == source_key and target.name != source.name:
554
+ shutil.rmtree(target) if target.is_dir() else target.unlink()
555
+
556
+
557
+ def copy_runtime_item(source: Path, target: Path) -> None:
558
+ if not source.exists():
559
+ return
560
+ if source.is_dir():
561
+ if target.exists():
562
+ shutil.rmtree(target)
563
+ shutil.copytree(source, target, dirs_exist_ok=True)
564
+ return
565
+ target.parent.mkdir(parents=True, exist_ok=True)
566
+ shutil.copy2(source, target)
567
+
568
+
569
+ def detect_environment(runtime_channel: str | None = None) -> dict[str, object]:
570
+ active_channel = runtime_channel or settings_manager.current.runtime_channel
571
+ cached = _environment_probe_cache.get(active_channel)
572
+ if cached is not None:
573
+ return dict(cached)
574
+
575
+ if uses_current_service_python(active_channel):
576
+ python_executable = Path(sys.executable).resolve()
577
+ probe_runner = lambda command, timeout=120: run_host_command(command, timeout=timeout)
578
+ else:
579
+ python_executable = runtime_python_executable(active_channel)
580
+ probe_runner = lambda command, timeout=120: run_command(command, runtime_channel=active_channel, timeout=timeout)
581
+ if python_executable is None:
582
+ payload = {
583
+ "pythonVersion": "",
584
+ "torchInstalled": False,
585
+ "torchVersion": "",
586
+ "cudaAvailable": False,
587
+ "gpuName": "",
588
+ "ytDlpVersion": "",
589
+ "localAsrInstalled": False,
590
+ "localAsrAvailable": False,
591
+ "localAsrVersion": "",
592
+ "chromadbInstalled": False,
593
+ "chromadbVersion": "",
594
+ "sentenceTransformersInstalled": False,
595
+ "sentenceTransformersVersion": "",
596
+ "knowledgeDependenciesReady": False,
597
+ "ffmpegLocation": "",
598
+ "recommendedModel": "base",
599
+ "recommendedDevice": "cpu",
600
+ "runtimeChannel": active_channel,
601
+ "runtimeReady": False,
602
+ "runtimePython": "",
603
+ }
604
+ _environment_probe_cache[active_channel] = dict(payload)
605
+ return payload
606
+
607
+ script = textwrap.dedent(
608
+ """
609
+ import importlib.metadata
610
+ import json
611
+ import sys
612
+ try:
613
+ import torch
614
+ except ImportError:
615
+ torch = None
616
+
617
+ cuda_available = bool(torch is not None and torch.cuda.is_available())
618
+ gpu_name = torch.cuda.get_device_name(0) if cuda_available else ""
619
+ payload = {
620
+ "pythonVersion": sys.version.split()[0],
621
+ "torchInstalled": torch is not None,
622
+ "torchVersion": torch.__version__ if torch is not None else "",
623
+ "cudaAvailable": cuda_available,
624
+ "gpuName": gpu_name,
625
+ "ytDlpVersion": importlib.metadata.version("yt-dlp"),
626
+ "localAsrVersion": "",
627
+ "localAsrInstalled": False,
628
+ "localAsrAvailable": False,
629
+ "chromadbVersion": "",
630
+ "chromadbInstalled": False,
631
+ "sentenceTransformersVersion": "",
632
+ "sentenceTransformersInstalled": False,
633
+ "knowledgeDependenciesReady": False,
634
+ "ffmpegLocation": "",
635
+ "recommendedModel": "large-v3-turbo" if cuda_available else "base",
636
+ "recommendedDevice": "cuda" if cuda_available else "cpu",
637
+ }
638
+ try:
639
+ payload["localAsrVersion"] = importlib.metadata.version("faster-whisper")
640
+ payload["localAsrInstalled"] = True
641
+ payload["localAsrAvailable"] = True
642
+ except importlib.metadata.PackageNotFoundError:
643
+ pass
644
+ try:
645
+ payload["chromadbVersion"] = importlib.metadata.version("chromadb")
646
+ payload["chromadbInstalled"] = True
647
+ except importlib.metadata.PackageNotFoundError:
648
+ pass
649
+ try:
650
+ payload["sentenceTransformersVersion"] = importlib.metadata.version("sentence-transformers")
651
+ payload["sentenceTransformersInstalled"] = True
652
+ except importlib.metadata.PackageNotFoundError:
653
+ pass
654
+ payload["knowledgeDependenciesReady"] = bool(
655
+ payload.get("chromadbInstalled") and payload.get("sentenceTransformersInstalled")
656
+ )
657
+ print(json.dumps(payload, ensure_ascii=False))
658
+ """
659
+ ).strip()
660
+
661
+ try:
662
+ result = probe_runner([str(python_executable), "-c", script], timeout=120)
663
+ payload = json.loads(result.stdout.strip() or "{}")
664
+ payload["ffmpegLocation"] = str(ffmpeg_location() or "")
665
+ _environment_probe_failures.pop(active_channel, None)
666
+ except Exception as exc:
667
+ failure_detail = (exc.stderr or exc.stdout or str(exc)).strip() if isinstance(exc, subprocess.CalledProcessError) else str(exc)
668
+ if _environment_probe_failures.get(active_channel) != failure_detail:
669
+ logger.warning(
670
+ "detect environment failed runtime_channel=%s error=%s detail=%s",
671
+ active_channel,
672
+ exc,
673
+ failure_detail[-1200:],
674
+ )
675
+ _environment_probe_failures[active_channel] = failure_detail
676
+ payload = {
677
+ "pythonVersion": "",
678
+ "torchInstalled": False,
679
+ "torchVersion": "",
680
+ "cudaAvailable": False,
681
+ "gpuName": "",
682
+ "ytDlpVersion": "",
683
+ "localAsrInstalled": False,
684
+ "localAsrAvailable": False,
685
+ "localAsrVersion": "",
686
+ "chromadbInstalled": False,
687
+ "chromadbVersion": "",
688
+ "sentenceTransformersInstalled": False,
689
+ "sentenceTransformersVersion": "",
690
+ "knowledgeDependenciesReady": False,
691
+ "ffmpegLocation": "",
692
+ "recommendedModel": "base",
693
+ "recommendedDevice": "cpu",
694
+ "runtimeError": failure_detail[-1200:],
695
+ }
696
+
697
+ payload.update(
698
+ {
699
+ "runtimeChannel": active_channel,
700
+ "runtimeReady": uses_current_service_python(active_channel) or runtime_python_executable(active_channel) is not None,
701
+ "runtimePython": str(python_executable),
702
+ "ffmpegLocation": str(ffmpeg_location() or ""),
703
+ }
704
+ )
705
+ payload["localAsrInstalled"] = bool(payload.get("localAsrInstalled"))
706
+ payload["localAsrAvailable"] = bool(payload.get("localAsrAvailable"))
707
+ payload["localAsrVersion"] = str(payload.get("localAsrVersion") or "")
708
+ payload["chromadbInstalled"] = bool(payload.get("chromadbInstalled"))
709
+ payload["chromadbVersion"] = str(payload.get("chromadbVersion") or "")
710
+ payload["sentenceTransformersInstalled"] = bool(payload.get("sentenceTransformersInstalled"))
711
+ payload["sentenceTransformersVersion"] = str(payload.get("sentenceTransformersVersion") or "")
712
+ payload["knowledgeDependenciesReady"] = bool(payload.get("knowledgeDependenciesReady"))
713
+ payload["runtimeError"] = str(payload.get("runtimeError") or "")
714
+ _environment_probe_cache[active_channel] = dict(payload)
715
+ return payload
716
+
717
+
718
+ def clear_environment_probe_cache(runtime_channel: str | None = None) -> None:
719
+ if runtime_channel is None:
720
+ _environment_probe_cache.clear()
721
+ _environment_probe_failures.clear()
722
+ return
723
+ _environment_probe_cache.pop(runtime_channel, None)
724
+ _environment_probe_failures.pop(runtime_channel, None)
725
+
726
+
727
+ def build_worker(
728
+ repository: SqliteTaskRepository,
729
+ current_settings: ServiceSettings,
730
+ environment_info: dict[str, object] | None = None,
731
+ ) -> TaskWorker:
732
+ selected_runtime_channel = current_settings.runtime_channel
733
+ if selected_runtime_channel != "base" and runtime_python_executable(selected_runtime_channel) is None:
734
+ logger.warning("runtime channel %s is not ready, falling back to base", selected_runtime_channel)
735
+ selected_runtime_channel = "base"
736
+
737
+ bootstrap_managed_runtime(selected_runtime_channel)
738
+ prepend_runtime_path(selected_runtime_channel)
739
+ activate_runtime_pythonpath(selected_runtime_channel)
740
+ environment = environment_info or detect_environment(selected_runtime_channel)
741
+ runtime_settings = current_settings.with_resolved_runtime(cuda_available=bool(environment.get("cudaAvailable")))
742
+ pipeline_settings = PipelineSettings(
743
+ tasks_dir=runtime_settings.tasks_dir,
744
+ runtime_channel=selected_runtime_channel,
745
+ transcription_provider=runtime_settings.transcription_provider,
746
+ whisper_model=runtime_settings.whisper_model,
747
+ whisper_device=runtime_settings.whisper_device,
748
+ whisper_compute_type=runtime_settings.whisper_compute_type,
749
+ local_asr_available=bool(environment.get("localAsrAvailable")),
750
+ siliconflow_asr_base_url=runtime_settings.siliconflow_asr_base_url,
751
+ siliconflow_asr_model=runtime_settings.siliconflow_asr_model,
752
+ siliconflow_asr_api_key=runtime_settings.siliconflow_asr_api_key,
753
+ llm_enabled=runtime_settings.llm_enabled,
754
+ llm_api_key=runtime_settings.llm_api_key,
755
+ llm_base_url=runtime_settings.llm_base_url,
756
+ llm_model=runtime_settings.llm_model,
757
+ summary_system_prompt=runtime_settings.summary_system_prompt,
758
+ summary_user_prompt_template=runtime_settings.summary_user_prompt_template,
759
+ mindmap_system_prompt=runtime_settings.mindmap_system_prompt,
760
+ mindmap_user_prompt_template=runtime_settings.mindmap_user_prompt_template,
761
+ summary_chunk_target_chars=runtime_settings.summary_chunk_target_chars,
762
+ summary_chunk_overlap_segments=runtime_settings.summary_chunk_overlap_segments,
763
+ summary_chunk_concurrency=runtime_settings.summary_chunk_concurrency,
764
+ summary_chunk_retry_count=runtime_settings.summary_chunk_retry_count,
765
+ ytdlp_cookies_file=runtime_settings.ytdlp_cookies_file,
766
+ ytdlp_cookies_browser=runtime_settings.ytdlp_cookies_browser,
767
+ )
768
+ return TaskWorker(
769
+ repository=repository,
770
+ pipeline_runner=RealPipelineRunner(pipeline_settings),
771
+ auto_generate_mindmap=current_settings.auto_generate_mindmap,
772
+ knowledge_index_auto_rebuild=(
773
+ current_settings.knowledge_index_auto_rebuild
774
+ if current_settings.knowledge_enabled
775
+ else "disabled"
776
+ ),
777
+ knowledge_index_settings=current_settings,
778
+ task_concurrency=current_settings.task_concurrency,
779
+ mindmap_concurrency=current_settings.mindmap_concurrency,
780
+ )
781
+
782
+
783
+ def replace_task_worker(app_state, next_worker: TaskWorker) -> TaskWorker:
784
+ previous_worker = getattr(app_state, "task_worker", None)
785
+ app_state.task_worker = next_worker
786
+ if isinstance(previous_worker, TaskWorker):
787
+ previous_worker.close_for_new_work()
788
+ return next_worker
789
+
790
+
791
+ def serialize_settings(
792
+ current_settings: ServiceSettings,
793
+ environment_info: dict[str, object] | None = None,
794
+ ) -> dict[str, object]:
795
+ environment = environment_info or detect_environment(current_settings.runtime_channel)
796
+ runtime_settings = current_settings.with_resolved_runtime(cuda_available=bool(environment.get("cudaAvailable")))
797
+ return {
798
+ "host": current_settings.host,
799
+ "port": current_settings.port,
800
+ "data_dir": str(current_settings.data_dir),
801
+ "cache_dir": str(current_settings.cache_dir),
802
+ "tasks_dir": str(current_settings.tasks_dir),
803
+ "database_url": current_settings.database_url,
804
+ "transcription_provider": current_settings.transcription_provider,
805
+ "whisper_model": runtime_settings.whisper_model,
806
+ "whisper_device": runtime_settings.whisper_device,
807
+ "whisper_compute_type": runtime_settings.whisper_compute_type,
808
+ "device_preference": current_settings.device_preference,
809
+ "compute_type": current_settings.compute_type,
810
+ "model_mode": current_settings.model_mode,
811
+ "fixed_model": current_settings.fixed_model,
812
+ "siliconflow_asr_base_url": current_settings.siliconflow_asr_base_url,
813
+ "siliconflow_asr_model": current_settings.siliconflow_asr_model,
814
+ "siliconflow_asr_api_key": current_settings.siliconflow_asr_api_key,
815
+ "siliconflow_asr_api_key_configured": bool(current_settings.siliconflow_asr_api_key),
816
+ "cuda_variant": current_settings.cuda_variant,
817
+ "runtime_channel": current_settings.runtime_channel,
818
+ "output_dir": str(current_settings.output_dir),
819
+ "preserve_temp_audio": current_settings.preserve_temp_audio,
820
+ "enable_cache": current_settings.enable_cache,
821
+ "language": current_settings.language,
822
+ "summary_mode": current_settings.summary_mode,
823
+ "llm_enabled": current_settings.llm_enabled,
824
+ "auto_generate_mindmap": current_settings.auto_generate_mindmap,
825
+ "llm_provider": current_settings.llm_provider,
826
+ "llm_base_url": current_settings.llm_base_url,
827
+ "llm_model": current_settings.llm_model,
828
+ "llm_api_key": current_settings.llm_api_key,
829
+ "llm_api_key_configured": bool(current_settings.llm_api_key),
830
+ "knowledge_llm_mode": current_settings.knowledge_llm_mode,
831
+ "knowledge_llm_enabled": current_settings.knowledge_llm_enabled,
832
+ "knowledge_llm_base_url": current_settings.knowledge_llm_base_url,
833
+ "knowledge_llm_model": current_settings.knowledge_llm_model,
834
+ "knowledge_llm_api_key": current_settings.knowledge_llm_api_key,
835
+ "knowledge_llm_api_key_configured": bool(current_settings.knowledge_llm_api_key),
836
+ "knowledge_enabled": current_settings.knowledge_enabled,
837
+ "knowledge_index_auto_rebuild": current_settings.knowledge_index_auto_rebuild,
838
+ "summary_system_prompt": current_settings.summary_system_prompt,
839
+ "summary_user_prompt_template": current_settings.summary_user_prompt_template,
840
+ "summary_chunk_target_chars": current_settings.summary_chunk_target_chars,
841
+ "summary_chunk_overlap_segments": current_settings.summary_chunk_overlap_segments,
842
+ "task_concurrency": current_settings.task_concurrency,
843
+ "mindmap_concurrency": current_settings.mindmap_concurrency,
844
+ "summary_chunk_concurrency": current_settings.summary_chunk_concurrency,
845
+ "summary_chunk_retry_count": current_settings.summary_chunk_retry_count,
846
+ "ytdlp_cookies_file": current_settings.ytdlp_cookies_file,
847
+ "ytdlp_cookies_browser": current_settings.ytdlp_cookies_browser,
848
+ "settings_file_exists": settings_manager.has_persisted_settings,
849
+ }
850
+
851
+
852
+ def install_cuda_support(cuda_variant: str, repository: SqliteTaskRepository) -> tuple[dict[str, object], TaskWorker]:
853
+ if cuda_variant not in {"cu124", "cu126", "cu128"}:
854
+ raise HTTPException(status_code=400, detail="Unsupported CUDA variant.")
855
+
856
+ runtime_channel = f"gpu-{cuda_variant}"
857
+ runtime_dir = ensure_runtime_channel(runtime_channel)
858
+ python_executable = runtime_python_executable(runtime_channel)
859
+ if runtime_dir is None or python_executable is None:
860
+ raise HTTPException(status_code=500, detail="Managed runtime is unavailable.")
861
+
862
+ try:
863
+ install_workspace_packages(python_executable, runtime_channel=runtime_channel)
864
+ ensure_runtime_pip(python_executable, runtime_channel)
865
+ result = torch_install_with_fallbacks(
866
+ python_executable,
867
+ runtime_channel,
868
+ cuda_variant,
869
+ timeout=1800,
870
+ runner=run_command,
871
+ )
872
+ except subprocess.CalledProcessError as exc:
873
+ clear_environment_probe_cache(runtime_channel)
874
+ raise HTTPException(status_code=500, detail=command_error_detail(exc, "安装 CUDA 依赖失败。")) from exc
875
+ except HTTPException:
876
+ clear_environment_probe_cache(runtime_channel)
877
+ raise
878
+
879
+ current_settings = settings_manager.save(SettingsUpdatePayload(cuda_variant=cuda_variant, runtime_channel=runtime_channel))
880
+ clear_environment_probe_cache(runtime_channel)
881
+ clear_environment_probe_cache("base")
882
+ write_runtime_metadata(runtime_channel, {"runtimeChannel": runtime_channel, "cudaVariant": cuda_variant, "python": str(python_executable)})
883
+ environment = detect_environment(runtime_channel)
884
+ worker = build_worker(repository, current_settings, environment_info=environment)
885
+ return {
886
+ "installed": True,
887
+ "cudaVariant": cuda_variant,
888
+ "runtimeChannel": runtime_channel,
889
+ "restartRequired": True,
890
+ "stdoutTail": (result.stdout or "")[-1500:],
891
+ "environment": environment,
892
+ }, worker
893
+
894
+
895
+ def install_local_asr(reinstall: bool, repository: SqliteTaskRepository) -> tuple[dict[str, object], TaskWorker]:
896
+ current_settings = settings_manager.current
897
+ runtime_channel = current_settings.runtime_channel
898
+ runtime_dir = ensure_runtime_channel(runtime_channel)
899
+ python_executable = runtime_python_executable(runtime_channel)
900
+ if runtime_dir is None or python_executable is None:
901
+ raise HTTPException(status_code=500, detail="Managed runtime is unavailable.")
902
+
903
+ try:
904
+ install_workspace_packages(python_executable, runtime_channel=runtime_channel)
905
+ ensure_runtime_pip(python_executable, runtime_channel)
906
+ result = pip_install_with_fallbacks(
907
+ python_executable,
908
+ runtime_channel,
909
+ ["faster-whisper>=1.1.1"],
910
+ package_label="本地 ASR 依赖",
911
+ reinstall=reinstall,
912
+ timeout=1800,
913
+ runner=run_command,
914
+ )
915
+ except subprocess.CalledProcessError as exc:
916
+ clear_environment_probe_cache(runtime_channel)
917
+ raise HTTPException(status_code=500, detail=((exc.stderr or exc.stdout or str(exc))[-1500:])) from exc
918
+ except HTTPException:
919
+ clear_environment_probe_cache(runtime_channel)
920
+ raise
921
+
922
+ clear_environment_probe_cache(runtime_channel)
923
+ environment = detect_environment(runtime_channel)
924
+ worker = build_worker(repository, current_settings, environment_info=environment)
925
+ write_runtime_metadata(
926
+ runtime_channel,
927
+ {
928
+ "runtimeChannel": runtime_channel,
929
+ "python": str(python_executable),
930
+ "localAsrInstalled": bool(environment.get("localAsrInstalled")),
931
+ "localAsrVersion": str(environment.get("localAsrVersion") or ""),
932
+ },
933
+ )
934
+ return {
935
+ "installed": bool(environment.get("localAsrInstalled")),
936
+ "runtimeChannel": runtime_channel,
937
+ "stdoutTail": ((result.stdout or "") + "\n" + (result.stderr or "")).strip()[-1500:],
938
+ "environment": environment,
939
+ }, worker
940
+
941
+
942
+ def install_knowledge_dependencies(
943
+ reinstall: bool,
944
+ repository: SqliteTaskRepository,
945
+ ) -> tuple[dict[str, object], TaskWorker]:
946
+ current_settings = settings_manager.current
947
+ runtime_channel = current_settings.runtime_channel
948
+ use_current_python = uses_current_service_python(runtime_channel)
949
+ if use_current_python:
950
+ runtime_dir = repo_root()
951
+ python_executable = Path(sys.executable).resolve()
952
+ runner = lambda command, runtime_channel, timeout=1800: run_host_command(command, timeout=timeout)
953
+ else:
954
+ runtime_dir = ensure_runtime_channel(runtime_channel)
955
+ python_executable = runtime_python_executable(runtime_channel)
956
+ runner = run_command
957
+ if runtime_dir is None or python_executable is None:
958
+ raise HTTPException(status_code=500, detail="Managed runtime is unavailable.")
959
+
960
+ packages = [
961
+ "chromadb>=1.0.0",
962
+ "sentence-transformers>=3.0",
963
+ ]
964
+
965
+ try:
966
+ if not use_current_python:
967
+ install_workspace_packages(python_executable, runtime_channel=runtime_channel)
968
+ ensure_runtime_pip(python_executable, runtime_channel)
969
+ result = pip_install_with_fallbacks(
970
+ python_executable,
971
+ runtime_channel,
972
+ packages,
973
+ package_label="知识库依赖",
974
+ reinstall=reinstall,
975
+ timeout=1800,
976
+ runner=runner,
977
+ )
978
+ except subprocess.CalledProcessError as exc:
979
+ clear_environment_probe_cache(runtime_channel)
980
+ raise HTTPException(status_code=500, detail=((exc.stderr or exc.stdout or str(exc))[-1500:])) from exc
981
+ except HTTPException:
982
+ clear_environment_probe_cache(runtime_channel)
983
+ raise
984
+
985
+ importlib.invalidate_caches()
986
+ activate_runtime_pythonpath(runtime_channel)
987
+ clear_environment_probe_cache(runtime_channel)
988
+ environment = detect_environment(runtime_channel)
989
+ worker = build_worker(repository, current_settings, environment_info=environment)
990
+ write_runtime_metadata(
991
+ runtime_channel,
992
+ {
993
+ "runtimeChannel": runtime_channel,
994
+ "python": str(python_executable),
995
+ "chromadbInstalled": bool(environment.get("chromadbInstalled")),
996
+ "chromadbVersion": str(environment.get("chromadbVersion") or ""),
997
+ "sentenceTransformersInstalled": bool(environment.get("sentenceTransformersInstalled")),
998
+ "sentenceTransformersVersion": str(environment.get("sentenceTransformersVersion") or ""),
999
+ "knowledgeDependenciesReady": bool(environment.get("knowledgeDependenciesReady")),
1000
+ },
1001
+ )
1002
+ return {
1003
+ "installed": bool(environment.get("knowledgeDependenciesReady")),
1004
+ "runtimeChannel": runtime_channel,
1005
+ "stdoutTail": ((result.stdout or "") + "\n" + (result.stderr or "")).strip()[-1500:],
1006
+ "environment": environment,
1007
+ }, worker