Repository navigation
Expand file tree
/
Copy pathclips-studio.spec
More file actions
374 lines (349 loc) · 16.2 KB
/
Copy pathclips-studio.spec
File metadata and controls
374 lines (349 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# PyInstaller spec — freezes the Python engine into resources/backend/api.exe
#
# Build with: python scripts/build_installer.py (don't call this directly)
#
# One-DIR, not one-file. One-file unpacks the whole payload to a temp folder
# on every launch, and with PyTorch in the bundle that is gigabytes of copying
# before the app answers its first request — slow and fragile. One-dir starts
# instantly and lets the installer share files properly.
#
# Console app on purpose: the backend prints its progress, and Electron reads
# or discards that. A windowed build sets sys.stdout to None, which turns
# every print() in the pipeline into a crash. Electron passes windowsHide so
# no console window is ever shown to the user.
import importlib.util
import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_all, collect_submodules
ROOT = Path(SPECPATH)
datas = []
binaries = []
hiddenimports = []
# Packages that resolve things at runtime rather than by import statement, so
# PyInstaller's static analysis cannot see them:
# yt_dlp — one extractor module per site, imported by name
# ultralytics — model/task registry, plus its config yaml
# faster_whisper / ctranslate2 — native libs + tokenizer assets
# curl_cffi — bundled libcurl binaries
# piper — espeak-ng phoneme data (19 MB) resolved from its own
# package directory at runtime, plus the espeak bridge
# onnxruntime — native inference libs Piper loads by name
# rapidocr_onnxruntime — its OCR models (15 MB) and config.yaml, read from
# its own package directory (on-screen text in a gaming stream)
# shapely / pyclipper — native libs RapidOCR's text detector needs
# nvidia.cublas — cuBLAS 12 (cublas64_12.dll), loaded by name by
# CTranslate2 at its first GPU run; PyTorch's CUDA 13 build
# only has cuBLAS 13 (transcription/transcriber.py, #111)
for package in ("yt_dlp", "yt_dlp_ejs", "ultralytics", "faster_whisper", "ctranslate2",
"curl_cffi", "piper", "onnxruntime", "rapidocr_onnxruntime",
"shapely", "pyclipper", "nvidia.cublas"):
pkg_datas, pkg_binaries, pkg_hidden = collect_all(package)
datas += pkg_datas
binaries += pkg_binaries
hiddenimports += pkg_hidden
# uvicorn picks its event loop, HTTP parser and websocket implementation at
# startup by importing strings, so every backend has to be pulled in.
hiddenimports += collect_submodules("uvicorn")
# Sports (sports/): each sport's package is imported by name from the registry
# (sports/__init__.py SPORTS), which the analyser can't follow.
hiddenimports += collect_submodules("sports")
# Plugins (plugins/): imported inside functions only when a job names a
# pipeline plugin, so name them all rather than rely on the analyser.
hiddenimports += collect_submodules("plugins")
# Reusable account providers and their desktop EventSub WebSocket client.
hiddenimports += collect_submodules("connected_accounts")
hiddenimports += collect_submodules("stream_workflows")
hiddenimports += collect_submodules("websockets")
# Clipping campaigns (campaigns/): every import of it sits inside a function.
hiddenimports += collect_submodules("campaigns")
# Cloud AI adapters (llm/providers/adapters/): adapter_for imports each by the
# name its provider spec gives, which the analyser can't follow. 2.0.0 shipped
# without them and saving any cloud key failed with a 500 (#135).
hiddenimports += collect_submodules("llm.providers.adapters")
hiddenimports += [
"uvicorn.logging",
"uvicorn.loops.auto",
"uvicorn.protocols.http.auto",
"uvicorn.protocols.websockets.auto",
"uvicorn.lifespan.on",
]
# Imported lazily inside functions, so they are invisible to the analyser.
hiddenimports += [
"torch",
"torchvision",
"cv2",
"psutil",
"pynvml",
"google.auth",
"google_auth_oauthlib",
"googleapiclient",
"googleapiclient.discovery",
# Gaming / Reaction: imported only when the switch is on (core/pipeline.py).
"gaming.run",
"gaming.detect",
"gaming.layout",
"gaming.framing",
"gaming.compose",
"gaming.panels",
# Scoring a gaming stream (imported only when it is asked for).
"analysis.gaming",
"analysis.chat_moments",
"analysis.game_audio",
"analysis.panns",
"analysis.game_text",
"analysis.game_vision",
# The second speaker's caption colour (imported only when it is ticked).
"analysis.voice_turns",
# Remote rendering (imported only when it is switched on).
"remote_render",
"remote_render.protocol",
"remote_render.settings",
"remote_render.tls",
"remote_render.queue",
"remote_render.gateway",
"remote_render.piece",
"remote_render.worker",
"remote_render.dispatch",
"remote_render.service",
"cryptography.x509",
"cryptography.hazmat.primitives.asymmetric.ec",
"sources.preview_frames",
# Runs a pipeline's Python script when main.py is started with the
# plugin marker (_clipskitty_script_host.py).
"_clipskitty_script_host",
]
# The whole standard library, for pipelines. They run on this Python
# (_clipskitty_script_host.py), and PyInstaller otherwise packs only the
# modules the engine itself happens to import, so an ordinary `import csv` or
# `import statistics` in a pipeline could fail in the installed app alone.
# The list comes from the Python doing the freezing, so it always matches it.
# Left out: GUI toolkits (tkinter is excluded below), the test suite, and
# tools for building or installing Python. tests/test_packaging.py guards it.
STDLIB_SKIP = {
"tkinter", "_tkinter", "turtle", "turtledemo", "idlelib", "test", "ensurepip", "venv", "lib2to3",
"distutils", "pydoc_data", "antigravity", "this",
}
for _name in sorted(sys.stdlib_module_names - STDLIB_SKIP):
if _name.startswith("__"):
continue
try:
_spec = importlib.util.find_spec(_name)
except (ImportError, ValueError):
_spec = None
if _spec is None: # not on this platform (POSIX-only modules on Windows, and so on)
continue
hiddenimports.append(_name)
if _spec.submodule_search_locations:
hiddenimports += collect_submodules(
_name, filter=lambda n: ".test" not in n and ".idle_test" not in n, on_error="ignore")
# Config the app reads from disk at runtime. Prompts especially: they are
# plain text on purpose so they can be tuned without touching code, and that
# only holds if they ship as files.
datas += [
(str(ROOT / "config" / "settings.yaml"), "config"),
(str(ROOT / "config" / "prompts"), "config/prompts"),
# What a gaming highlight looks like per kind of game, and chat's emote
# classes (analysis/gaming.py), for scoring a gaming stream.
(str(ROOT / "config" / "gaming.yaml"), "config"),
# Each sport's moments, their worth and windows, and the commentary words
# that mark them (sports/, the Sports toggle).
(str(ROOT / "config" / "sports.yaml"), "config"),
# The plugin SDK as plain files: plugins/_sdk.py imports it from here, and
# every plugin process gets this folder on its PYTHONPATH (plugins/runner.py).
(str(ROOT / "sdk" / "python" / "clipskitty_sdk"), "sdk/python/clipskitty_sdk"),
# The SDK is MIT, unlike the rest of the app: its licence travels with it.
(str(ROOT / "sdk" / "python" / "LICENSE"), "sdk/python"),
# The official modes' manifests (plugins/builtin/), listed beside plugins.
(str(ROOT / "plugins" / "builtin"), "plugins/builtin"),
# The copy of Awesome Clips Kitty's index that ships with the app
# (plugins/registry.py bundled_path): the Marketplace's listings and the
# block list before the app has been online, and the copy it trusts.
(str(ROOT / "plugins" / "catalog_index.json"), "plugins"),
# The sound tagger's 527 class names, in its output order (analysis/panns.py).
(str(ROOT / "config" / "audioset_labels.txt"), "config"),
# The three-second clip a voice model is checked with before it is used
# for online transcription (transcription/cloud.check_model).
(str(ROOT / "transcription" / "assets"), "transcription/assets"),
# The app version, for bug reports. ui/package.json is the only place the
# version is written, and in a frozen build it is not on disk beside the
# code — so every report from an installed copy said "app": "?" and could
# not be dated. Three reports arrived that way before anyone noticed.
# Bundling the file keeps one source of truth rather than stamping the
# version into a generated module that could drift from it.
(str(ROOT / "ui" / "package.json"), "."),
# Prebuilt end cards, one per clip format the pipeline actually emits
# (two canvases x 30/60fps). video/outro.py falls back to rendering one,
# which takes ~20s — bundling these means the common paths never wait,
# and video/mascot_art.py is imported rather than scripts/, which is not
# packaged at all.
(str(ROOT / "assets" / "outro"), "assets/outro"),
# Gaming / Reaction's layouts and platform safe zones, read beside
# gaming/framing.py at runtime (the editor has its own checked copy).
(str(ROOT / "gaming" / "layouts.json"), "gaming"),
]
# OpenCV Haar cascades. cv2 is a hidden import above, which ships the MODULE
# and not its data directory -- so cv2.data.haarcascades resolves to a path
# that does not exist in an installed build. CascadeClassifier does not raise
# on a missing file; it returns an empty classifier and detectMultiScale then
# fails with "(-215:Assertion failed) !empty()", which reads as a rendering
# bug. That was issue #86: a job analysed a video, chose a clip, and produced
# zero clips because the crop needed the face fallback.
#
# Only the two files video/tracker.py actually loads. collect_all("cv2") would
# bring the whole data directory -- cascades for eyes, plates, bodies and the
# rest -- none of which this app ever opens, in an installer already near 7 GB.
try:
import cv2 as _cv2
_cascade_dir = Path(_cv2.data.haarcascades)
for _cascade in ("haarcascade_frontalface_default.xml",
"haarcascade_profileface.xml"):
_src = _cascade_dir / _cascade
if _src.is_file():
datas += [(str(_src), "cascades")]
except Exception:
# A build machine without cv2 cannot produce a working app anyway, and the
# runtime now degrades to pose-only cropping rather than crashing.
pass
# YOLO weights. Ultralytics would otherwise download them on first use, which
# means a creator's first video stalls on a silent network fetch.
for weights in ("yolov8n-pose.pt", "yolov8n.pt"):
if (ROOT / weights).exists():
datas += [(str(ROOT / weights), ".")]
# Active-speaker weights (TalkNet). Same reasoning as the YOLO ones: bundled
# so an installed copy never fetches anything at runtime. Absent from a fresh
# checkout until scripts/fetch_asd_model.py runs; the build carries on without
# it and the tracker falls back to mouth-motion.
_asd = ROOT / "models" / "pretrain_TalkSet.model"
if _asd.exists():
datas += [(str(_asd), ".")]
# The basketball detector (E-BARD's YOLOv8n, 6 MB), fetched by
# scripts/fetch_basketball_model.py, for sports/basketball/crop. Without it a
# basketball clip is framed with the COCO model and the rim finder.
_bball = ROOT / "models" / "BODD_yolov8n_0001.pt"
if _bball.exists():
datas += [(str(_bball), ".")]
# Game-sound weights (PANNs MobileNetV1, 24 MB), fetched by
# scripts/fetch_panns.py. Without them a gaming stream is scored on chat and
# the streamer's voice only.
_panns = ROOT / "models" / "panns_mobilenetv1.pth"
if _panns.exists():
datas += [(str(_panns), ".")]
# Voice models (pyannote segmentation 6 MB, WeSpeaker ResNet34-LM 27 MB),
# fetched by scripts/fetch_voice_model.py, for analysis/voice_turns.py. Without
# both, captions stay one colour whoever is talking.
for _voice in ("pyannote_segmentation_3.onnx", "wespeaker_resnet34_lm.onnx"):
if (ROOT / "models" / _voice).exists():
datas += [(str(ROOT / "models" / _voice), ".")]
# FFmpeg, fetched by scripts/fetch_ffmpeg.py. core.binaries looks for an
# ffmpeg/ folder next to the executable.
vendor_ffmpeg = ROOT / "vendor" / "ffmpeg"
if vendor_ffmpeg.exists():
for binary in vendor_ffmpeg.iterdir():
if binary.is_file():
datas += [(str(binary), "ffmpeg")]
# QuickJS-ng, fetched by scripts/fetch_quickjs.py: runs YouTube's player
# JavaScript for the YouTube cookies fallback only. core.binaries.quickjs looks
# for a quickjs/ folder next to the executable.
vendor_quickjs = ROOT / "vendor" / "quickjs"
if vendor_quickjs.exists():
for binary in vendor_quickjs.iterdir():
if binary.is_file():
datas += [(str(binary), "quickjs")]
# The Ollama runtime, fetched by scripts/fetch_ollama.py. Bundled so a creator
# never has to install a second program before they can make a clip.
#
# Walked recursively, unlike FFmpeg's two flat binaries: ollama.exe is useless
# without the GPU runners and CUDA libraries under lib/, and those have to keep
# their layout relative to the executable or it falls back to CPU inference.
vendor_ollama = ROOT / "vendor" / "ollama"
if vendor_ollama.exists():
for item in vendor_ollama.rglob("*"):
if item.is_file():
datas += [(str(item), str(Path("ollama") / item.relative_to(vendor_ollama).parent))]
# Whisper weights, fetched by scripts/fetch_whisper.py. The quietest of the
# bundled dependencies and the one that most needed bundling: faster-whisper
# reads a bare size name as "download it from Hugging Face", so without these
# a creator's first video stalls on a silent multi-gigabyte fetch that is
# indistinguishable from a hang. core.binaries.whisper_model() finds them.
vendor_whisper = ROOT / "vendor" / "whisper"
if vendor_whisper.exists():
for item in vendor_whisper.rglob("*"):
if item.is_file():
datas += [(str(item), str(Path("whisper") / item.relative_to(vendor_whisper).parent))]
# Things that bloat the bundle without being used. Matplotlib and friends
# arrive as transitive dependencies of ultralytics but nothing here plots.
#
# polars is 175 MB of that, and ultralytics declares it as a hard dependency,
# so dropping it is only safe because it is never actually imported on the
# paths this app uses. Verified rather than assumed: with polars blocked at
# import time, both YOLO models load and predict, and polars never appears in
# sys.modules. Re-check after an ultralytics upgrade — a detect/pose run that
# starts wanting a dataframe would fail at runtime, not at build time.
#
# matplotlib is NOT here, and must not be added back. Ultralytics imports it
# on the path video/tracker.py loads the model through, so excluding it made
# every clip job die at the reactions stage with "No module named
# 'matplotlib'" — in installed copies only, because a development machine has
# it sitting in site-packages. It cost a release to find. tests/test_packaging
# guards it.
excludes = [
"polars",
"tkinter",
"PyQt5",
"PySide2",
"IPython",
"jupyter",
"notebook",
"pandas",
"sklearn",
"scikeras",
"tensorflow",
"keras",
]
# keyring keeps Connected Accounts tokens outside Windows only; on Windows they
# go through core/secrets (DPAPI). requirements.txt doesn't install it there,
# and connected_accounts/store.py imports it only off Windows, so make sure
# the analyser never follows that import into the Windows build.
if sys.platform == "win32":
excludes.append("keyring")
a = Analysis(
[str(ROOT / "main.py")],
pathex=[str(ROOT)],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=excludes,
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="api",
debug=False,
bootloader_ignore_signals=False,
strip=False,
# UPX corrupts some CUDA and OpenCV DLLs and saves little on a bundle
# this size — the risk is not worth it.
upx=False,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=False,
upx_exclude=[],
name="backend",
)