Ctrl K
ComunidadGuías…

clips-studio.spec

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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",
)