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,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
|