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