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,485 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
APP_SLUG = "bilisum"
|
|
13
|
+
LEGACY_APP_SLUG = "briefvid"
|
|
14
|
+
DOCKER_DATA_ROOT = Path("/data")
|
|
15
|
+
_DLL_DIRECTORY_HANDLES: dict[str, object] = {}
|
|
16
|
+
_LEGACY_APP_DATA_MIGRATION_DONE = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _env_flag(name: str) -> bool:
|
|
20
|
+
value = os.environ.get(name, "").strip().lower()
|
|
21
|
+
return value in {"1", "true", "yes", "on"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_running_in_docker() -> bool:
|
|
25
|
+
return _env_flag("VIDEO_SUM_DOCKER") or Path("/.dockerenv").exists()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_frozen() -> bool:
|
|
29
|
+
return bool(getattr(sys, "frozen", False))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def repo_root() -> Path:
|
|
33
|
+
return Path(__file__).resolve().parents[4]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def bundled_root() -> Path:
|
|
37
|
+
if is_frozen():
|
|
38
|
+
meipass = getattr(sys, "_MEIPASS", "")
|
|
39
|
+
if meipass:
|
|
40
|
+
return Path(meipass).resolve()
|
|
41
|
+
return Path(sys.executable).resolve().parent
|
|
42
|
+
return repo_root()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def web_static_dir() -> Path:
|
|
46
|
+
override = os.environ.get("VIDEO_SUM_WEB_STATIC_DIR", "").strip()
|
|
47
|
+
if override:
|
|
48
|
+
return Path(override).resolve()
|
|
49
|
+
if is_frozen():
|
|
50
|
+
return bundled_root() / "web" / "static"
|
|
51
|
+
return repo_root() / "apps" / "web" / "static"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def bundled_bin_dir() -> Path:
|
|
55
|
+
if is_frozen():
|
|
56
|
+
return bundled_root() / "bin"
|
|
57
|
+
return repo_root() / "bin"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def local_appdata_dir() -> Path:
|
|
61
|
+
local_appdata = os.environ.get("LOCALAPPDATA")
|
|
62
|
+
if local_appdata:
|
|
63
|
+
return Path(local_appdata)
|
|
64
|
+
return Path.home() / "AppData" / "Local"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _copy_missing_tree(source: Path, destination: Path) -> None:
|
|
68
|
+
if not source.exists():
|
|
69
|
+
return
|
|
70
|
+
if source.is_dir():
|
|
71
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
for child in source.iterdir():
|
|
73
|
+
_copy_missing_tree(child, destination / child.name)
|
|
74
|
+
return
|
|
75
|
+
if source.is_file() and not destination.exists():
|
|
76
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
shutil.copy2(source, destination)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def migrate_legacy_app_data_root() -> None:
|
|
81
|
+
global _LEGACY_APP_DATA_MIGRATION_DONE
|
|
82
|
+
if _LEGACY_APP_DATA_MIGRATION_DONE:
|
|
83
|
+
return
|
|
84
|
+
_LEGACY_APP_DATA_MIGRATION_DONE = True
|
|
85
|
+
if is_running_in_docker() or os.environ.get("VIDEO_SUM_APP_DATA_ROOT", "").strip():
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
source = local_appdata_dir() / LEGACY_APP_SLUG
|
|
89
|
+
destination = local_appdata_dir() / APP_SLUG
|
|
90
|
+
if not source.exists() or source == destination:
|
|
91
|
+
return
|
|
92
|
+
try:
|
|
93
|
+
_copy_missing_tree(source, destination)
|
|
94
|
+
except OSError:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def app_data_root() -> Path:
|
|
99
|
+
override = os.environ.get("VIDEO_SUM_APP_DATA_ROOT", "").strip()
|
|
100
|
+
if override:
|
|
101
|
+
return Path(override).resolve()
|
|
102
|
+
if is_running_in_docker():
|
|
103
|
+
return DOCKER_DATA_ROOT
|
|
104
|
+
migrate_legacy_app_data_root()
|
|
105
|
+
return local_appdata_dir() / APP_SLUG
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def default_data_dir() -> Path:
|
|
109
|
+
if is_running_in_docker():
|
|
110
|
+
return app_data_root()
|
|
111
|
+
return app_data_root() / "data"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def default_cache_dir() -> Path:
|
|
115
|
+
return default_data_dir() / "cache"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def default_tasks_dir() -> Path:
|
|
119
|
+
return default_data_dir() / "tasks"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def default_database_url() -> str:
|
|
123
|
+
return f"sqlite:///{(default_data_dir() / 'video_sum.db').as_posix()}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def default_host() -> str:
|
|
127
|
+
if is_running_in_docker():
|
|
128
|
+
return "0.0.0.0"
|
|
129
|
+
return "127.0.0.1"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def managed_runtime_root() -> Path:
|
|
133
|
+
return app_data_root() / "runtime"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def managed_runtime_dir(runtime_channel: str) -> Path:
|
|
137
|
+
return managed_runtime_root() / runtime_channel
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def log_dir() -> Path:
|
|
141
|
+
return app_data_root() / "logs"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def service_log_path() -> Path:
|
|
145
|
+
return log_dir() / "service.log"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def bundled_runtime_seed_dir() -> Path:
|
|
149
|
+
return bundled_root() / "runtime" / "base"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def runtime_seed_available() -> bool:
|
|
153
|
+
return bundled_runtime_seed_dir().exists()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def read_runtime_metadata(target_dir: Path) -> dict[str, object]:
|
|
157
|
+
metadata_path = target_dir / "video_sum_runtime.json"
|
|
158
|
+
if not metadata_path.exists():
|
|
159
|
+
return {}
|
|
160
|
+
try:
|
|
161
|
+
payload = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
162
|
+
except (OSError, ValueError):
|
|
163
|
+
return {}
|
|
164
|
+
return payload if isinstance(payload, dict) else {}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def bundled_runtime_seed_metadata() -> dict[str, object]:
|
|
168
|
+
return read_runtime_metadata(bundled_runtime_seed_dir())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def runtime_python_candidates(runtime_dir: Path) -> list[Path]:
|
|
172
|
+
"""返回 Python 解释器候选路径列表。
|
|
173
|
+
|
|
174
|
+
优先查找根目录的 python.exe(可重定位的解释器),
|
|
175
|
+
其次才是 Scripts 目录的 venv 启动器(可能硬编码了原始路径)。
|
|
176
|
+
"""
|
|
177
|
+
return [
|
|
178
|
+
runtime_dir / "python.exe", # 优先:根目录的可重定位解释器
|
|
179
|
+
runtime_dir / "Scripts" / "python.exe", # 次选:venv 启动器(可能硬编码路径)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def runtime_scripts_dir(runtime_dir: Path) -> Path:
|
|
184
|
+
scripts_dir = runtime_dir / "Scripts"
|
|
185
|
+
if scripts_dir.exists():
|
|
186
|
+
return scripts_dir
|
|
187
|
+
return runtime_dir
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def runtime_site_packages_dir(runtime_channel: str) -> Path:
|
|
191
|
+
return managed_runtime_dir(runtime_channel) / "Lib" / "site-packages"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def runtime_stdlib_dir(runtime_channel: str) -> Path:
|
|
195
|
+
return managed_runtime_dir(runtime_channel) / "stdlib"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def runtime_pythonpath_dirs(runtime_channel: str) -> list[Path]:
|
|
199
|
+
runtime_dir = managed_runtime_dir(runtime_channel)
|
|
200
|
+
candidates = [
|
|
201
|
+
runtime_stdlib_dir(runtime_channel),
|
|
202
|
+
runtime_dir / "DLLs",
|
|
203
|
+
runtime_site_packages_dir(runtime_channel),
|
|
204
|
+
runtime_dir,
|
|
205
|
+
]
|
|
206
|
+
return [candidate for candidate in candidates if candidate.exists()]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def runtime_library_dirs(runtime_channel: str) -> list[Path]:
|
|
210
|
+
runtime_dir = managed_runtime_dir(runtime_channel)
|
|
211
|
+
candidates: list[Path] = []
|
|
212
|
+
|
|
213
|
+
python_executable = runtime_python_executable(runtime_channel)
|
|
214
|
+
if python_executable is not None:
|
|
215
|
+
candidates.append(python_executable.parent)
|
|
216
|
+
portable_dlls_dir = runtime_dir / "DLLs"
|
|
217
|
+
if portable_dlls_dir.exists():
|
|
218
|
+
candidates.append(portable_dlls_dir)
|
|
219
|
+
candidates.append(runtime_scripts_dir(runtime_dir))
|
|
220
|
+
|
|
221
|
+
torch_lib_dir = runtime_dir / "Lib" / "site-packages" / "torch" / "lib"
|
|
222
|
+
if torch_lib_dir.exists():
|
|
223
|
+
candidates.append(torch_lib_dir)
|
|
224
|
+
|
|
225
|
+
nvidia_root = runtime_dir / "Lib" / "site-packages" / "nvidia"
|
|
226
|
+
if nvidia_root.exists():
|
|
227
|
+
for bin_dir in sorted(nvidia_root.rglob("bin")):
|
|
228
|
+
if bin_dir.is_dir():
|
|
229
|
+
candidates.append(bin_dir)
|
|
230
|
+
|
|
231
|
+
cuda_path = os.environ.get("CUDA_PATH")
|
|
232
|
+
if cuda_path:
|
|
233
|
+
cuda_bin_dir = Path(cuda_path) / "bin"
|
|
234
|
+
if cuda_bin_dir.exists():
|
|
235
|
+
candidates.append(cuda_bin_dir)
|
|
236
|
+
|
|
237
|
+
unique_dirs: list[Path] = []
|
|
238
|
+
seen: set[str] = set()
|
|
239
|
+
for candidate in candidates:
|
|
240
|
+
try:
|
|
241
|
+
resolved = candidate.resolve()
|
|
242
|
+
except OSError:
|
|
243
|
+
resolved = candidate
|
|
244
|
+
key = str(resolved).lower()
|
|
245
|
+
if not candidate.exists() or key in seen:
|
|
246
|
+
continue
|
|
247
|
+
seen.add(key)
|
|
248
|
+
unique_dirs.append(resolved)
|
|
249
|
+
return unique_dirs
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def runtime_worker_executable(runtime_channel: str) -> Path | None:
|
|
253
|
+
runtime_dir = managed_runtime_dir(runtime_channel)
|
|
254
|
+
scripts_dir = runtime_scripts_dir(runtime_dir)
|
|
255
|
+
candidates = [
|
|
256
|
+
scripts_dir / "video-sum-transcribe-worker.exe",
|
|
257
|
+
scripts_dir / "video-sum-transcribe-worker",
|
|
258
|
+
]
|
|
259
|
+
for candidate in candidates:
|
|
260
|
+
if candidate.exists():
|
|
261
|
+
return candidate
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def runtime_python_executable(runtime_channel: str) -> Path | None:
|
|
266
|
+
"""获取托管运行时 Python 解释器路径。
|
|
267
|
+
|
|
268
|
+
优先查找根目录的 python.exe(可重定位的解释器),
|
|
269
|
+
其次才是 Scripts 目录的 venv 启动器。
|
|
270
|
+
|
|
271
|
+
注意:不再回退到 sys.executable,因为在生产环境中这可能导致
|
|
272
|
+
使用开发环境的 Python 路径。
|
|
273
|
+
"""
|
|
274
|
+
runtime_dir = managed_runtime_dir(runtime_channel)
|
|
275
|
+
for candidate in runtime_python_candidates(runtime_dir):
|
|
276
|
+
if candidate.exists():
|
|
277
|
+
return candidate
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def ffmpeg_location() -> Path | None:
|
|
282
|
+
"""返回 ffmpeg 可执行文件的路径(不是目录)。"""
|
|
283
|
+
# 1. 环境变量指定的目录
|
|
284
|
+
env_dir = os.environ.get("VIDEO_SUM_FFMPEG_DIR")
|
|
285
|
+
if env_dir:
|
|
286
|
+
candidate = Path(env_dir)
|
|
287
|
+
if candidate.exists():
|
|
288
|
+
ffmpeg_exe = candidate / "ffmpeg.exe"
|
|
289
|
+
if ffmpeg_exe.exists():
|
|
290
|
+
return ffmpeg_exe
|
|
291
|
+
|
|
292
|
+
# 2. 打包后的 bin 目录
|
|
293
|
+
bundled_dir = bundled_bin_dir()
|
|
294
|
+
if bundled_dir.exists():
|
|
295
|
+
ffmpeg_exe = bundled_dir / "ffmpeg.exe"
|
|
296
|
+
if ffmpeg_exe.exists():
|
|
297
|
+
return ffmpeg_exe
|
|
298
|
+
|
|
299
|
+
# 3. 系统 PATH 中的 ffmpeg
|
|
300
|
+
ffmpeg_path = shutil.which("ffmpeg")
|
|
301
|
+
if ffmpeg_path:
|
|
302
|
+
return Path(ffmpeg_path).resolve()
|
|
303
|
+
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def prepend_runtime_path(runtime_channel: str) -> None:
|
|
308
|
+
paths = [str(path) for path in runtime_library_dirs(runtime_channel)]
|
|
309
|
+
ffmpeg_exe = ffmpeg_location()
|
|
310
|
+
if ffmpeg_exe is not None:
|
|
311
|
+
# 添加 ffmpeg 所在目录到 PATH
|
|
312
|
+
paths.append(str(ffmpeg_exe.parent))
|
|
313
|
+
|
|
314
|
+
current_path = os.environ.get("PATH", "")
|
|
315
|
+
for value in reversed(paths):
|
|
316
|
+
if value and value not in current_path:
|
|
317
|
+
current_path = f"{value}{os.pathsep}{current_path}" if current_path else value
|
|
318
|
+
os.environ["PATH"] = current_path
|
|
319
|
+
activate_runtime_dll_directories(runtime_channel)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def activate_runtime_pythonpath(runtime_channel: str) -> None:
|
|
323
|
+
runtime_paths = runtime_pythonpath_dirs(runtime_channel)
|
|
324
|
+
if not runtime_paths:
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
runtime_root = managed_runtime_root()
|
|
328
|
+
try:
|
|
329
|
+
resolved_runtime_root = runtime_root.resolve()
|
|
330
|
+
except OSError:
|
|
331
|
+
resolved_runtime_root = runtime_root
|
|
332
|
+
|
|
333
|
+
filtered: list[str] = []
|
|
334
|
+
for entry in sys.path:
|
|
335
|
+
if not entry:
|
|
336
|
+
filtered.append(entry)
|
|
337
|
+
continue
|
|
338
|
+
try:
|
|
339
|
+
entry_path = Path(entry).resolve()
|
|
340
|
+
except OSError:
|
|
341
|
+
filtered.append(entry)
|
|
342
|
+
continue
|
|
343
|
+
try:
|
|
344
|
+
inside_runtime_root = entry_path.is_relative_to(resolved_runtime_root)
|
|
345
|
+
except ValueError:
|
|
346
|
+
inside_runtime_root = False
|
|
347
|
+
is_managed_runtime_path = inside_runtime_root and (
|
|
348
|
+
entry_path.name in {"stdlib", "DLLs"}
|
|
349
|
+
or entry_path.parent == resolved_runtime_root
|
|
350
|
+
or (entry_path.name == "site-packages" and entry_path.parent.name == "Lib")
|
|
351
|
+
)
|
|
352
|
+
if not is_managed_runtime_path:
|
|
353
|
+
filtered.append(entry)
|
|
354
|
+
|
|
355
|
+
for runtime_path in runtime_paths:
|
|
356
|
+
value = str(runtime_path)
|
|
357
|
+
if value not in filtered:
|
|
358
|
+
filtered.append(value)
|
|
359
|
+
sys.path[:] = filtered
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def activate_runtime_dll_directories(runtime_channel: str) -> None:
|
|
363
|
+
add_dll_directory = getattr(os, "add_dll_directory", None)
|
|
364
|
+
if add_dll_directory is None:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
resolved_runtime_root = managed_runtime_root().resolve()
|
|
369
|
+
except OSError:
|
|
370
|
+
resolved_runtime_root = managed_runtime_root()
|
|
371
|
+
|
|
372
|
+
desired_dirs: list[Path] = runtime_library_dirs(runtime_channel)
|
|
373
|
+
desired_keys: set[str] = set()
|
|
374
|
+
for directory in desired_dirs:
|
|
375
|
+
try:
|
|
376
|
+
resolved = directory.resolve()
|
|
377
|
+
except OSError:
|
|
378
|
+
resolved = directory
|
|
379
|
+
desired_keys.add(str(resolved).lower())
|
|
380
|
+
|
|
381
|
+
for key, handle in list(_DLL_DIRECTORY_HANDLES.items()):
|
|
382
|
+
path = Path(key)
|
|
383
|
+
try:
|
|
384
|
+
inside_runtime_root = path.is_relative_to(resolved_runtime_root)
|
|
385
|
+
except ValueError:
|
|
386
|
+
inside_runtime_root = False
|
|
387
|
+
if inside_runtime_root and key not in desired_keys:
|
|
388
|
+
close = getattr(handle, "close", None)
|
|
389
|
+
if close is not None:
|
|
390
|
+
close()
|
|
391
|
+
_DLL_DIRECTORY_HANDLES.pop(key, None)
|
|
392
|
+
|
|
393
|
+
for directory in desired_dirs:
|
|
394
|
+
if not directory.exists():
|
|
395
|
+
continue
|
|
396
|
+
try:
|
|
397
|
+
resolved = directory.resolve()
|
|
398
|
+
except OSError:
|
|
399
|
+
resolved = directory
|
|
400
|
+
key = str(resolved).lower()
|
|
401
|
+
if key in _DLL_DIRECTORY_HANDLES:
|
|
402
|
+
continue
|
|
403
|
+
try:
|
|
404
|
+
_DLL_DIRECTORY_HANDLES[key] = add_dll_directory(str(resolved))
|
|
405
|
+
except OSError:
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def bootstrap_managed_runtime(runtime_channel: str = "base") -> Path | None:
|
|
410
|
+
runtime_dir = managed_runtime_dir(runtime_channel)
|
|
411
|
+
if runtime_channel != "base" or not is_frozen() or not runtime_seed_available():
|
|
412
|
+
if runtime_python_executable(runtime_channel) is not None:
|
|
413
|
+
return runtime_dir
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
seed_dir = bundled_runtime_seed_dir()
|
|
417
|
+
seed_metadata = bundled_runtime_seed_metadata()
|
|
418
|
+
runtime_metadata = read_runtime_metadata(runtime_dir)
|
|
419
|
+
runtime_ready = runtime_python_executable(runtime_channel) is not None
|
|
420
|
+
seed_version = str(seed_metadata.get("appVersion") or "")
|
|
421
|
+
runtime_version = str(runtime_metadata.get("appVersion") or "")
|
|
422
|
+
requires_refresh = (
|
|
423
|
+
not runtime_ready
|
|
424
|
+
or runtime_version != seed_version
|
|
425
|
+
or runtime_metadata.get("runtimeLayout") != seed_metadata.get("runtimeLayout")
|
|
426
|
+
)
|
|
427
|
+
if not requires_refresh:
|
|
428
|
+
return runtime_dir
|
|
429
|
+
|
|
430
|
+
runtime_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
431
|
+
if runtime_dir.exists():
|
|
432
|
+
shutil.rmtree(runtime_dir)
|
|
433
|
+
shutil.copytree(seed_dir, runtime_dir, dirs_exist_ok=True)
|
|
434
|
+
return runtime_dir
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def runtime_metadata_path(runtime_channel: str) -> Path:
|
|
438
|
+
return managed_runtime_dir(runtime_channel) / "video_sum_runtime.json"
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def write_runtime_metadata(runtime_channel: str, payload: dict[str, object]) -> None:
|
|
442
|
+
target = runtime_metadata_path(runtime_channel)
|
|
443
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
444
|
+
current = read_runtime_metadata(target.parent)
|
|
445
|
+
target.write_text(
|
|
446
|
+
json.dumps({**current, **payload}, ensure_ascii=False, indent=2),
|
|
447
|
+
encoding="utf-8",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _get_windows_dll_directory() -> str | None:
|
|
452
|
+
if os.name != "nt":
|
|
453
|
+
return None
|
|
454
|
+
kernel32 = ctypes.windll.kernel32
|
|
455
|
+
kernel32.GetDllDirectoryW.restype = ctypes.c_uint32
|
|
456
|
+
needed = kernel32.GetDllDirectoryW(0, None)
|
|
457
|
+
if needed == 0:
|
|
458
|
+
return None
|
|
459
|
+
buffer = ctypes.create_unicode_buffer(needed)
|
|
460
|
+
kernel32.GetDllDirectoryW(needed, buffer)
|
|
461
|
+
return buffer.value
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _set_windows_dll_directory(path: str | None) -> None:
|
|
465
|
+
if os.name != "nt":
|
|
466
|
+
return
|
|
467
|
+
kernel32 = ctypes.windll.kernel32
|
|
468
|
+
kernel32.SetDllDirectoryW.argtypes = [ctypes.c_wchar_p]
|
|
469
|
+
kernel32.SetDllDirectoryW.restype = ctypes.c_int
|
|
470
|
+
if not kernel32.SetDllDirectoryW(path):
|
|
471
|
+
raise OSError(ctypes.get_last_error(), "SetDllDirectoryW failed")
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@contextmanager
|
|
475
|
+
def sanitized_subprocess_dll_search():
|
|
476
|
+
if os.name != "nt" or not is_frozen():
|
|
477
|
+
yield
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
previous = _get_windows_dll_directory()
|
|
481
|
+
_set_windows_dll_directory(None)
|
|
482
|
+
try:
|
|
483
|
+
yield
|
|
484
|
+
finally:
|
|
485
|
+
_set_windows_dll_directory(previous)
|