Skip to content

Commit c2171fa

Browse files
committed
[dist] Update Windows dependencies, drop MoviePy
MoviePy is non-VFR compliant and rarely used. It might be useful for some types of inputs however so if we can sort out the duplicated ffmpeg.exe issue before release we can add it back in, otherwise it's a stretch goal for now.
1 parent 217de5d commit c2171fa

16 files changed

Lines changed: 92 additions & 30 deletions

.github/workflows/build-windows.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ jobs:
3636
python-version: ["3.13"]
3737

3838
env:
39-
ffmpeg-version: "7.1"
40-
IMAGEIO_FFMPEG_EXE: ""
39+
ffmpeg-version: "8.1"
4140

4241
steps:
4342
- uses: actions/checkout@v5
@@ -70,7 +69,11 @@ jobs:
7069
shell: bash
7170
run: |
7271
7z e ffmpeg-${{ env.ffmpeg-version }}-full_build.7z ffmpeg.exe -r
73-
echo "IMAGEIO_FFMPEG_EXE=`realpath ffmpeg.exe`" >> "$GITHUB_ENV"
72+
export PATH="$(pwd):$PATH"
73+
# moviepy.config resolves ffmpeg via imageio_ffmpeg at import time; `--no-binary`
74+
# strips the bundled binary, so point at the GyanD ffmpeg we just extracted
75+
# for both pytest and the subsequent pyinstaller step.
76+
echo "IMAGEIO_FFMPEG_EXE=$(realpath ffmpeg.exe)" >> "$GITHUB_ENV"
7477
python -m pytest -vv
7578
7679
- name: Build PySceneDetect

.github/workflows/publish-pypi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
core.setFailed(`Workflow "${workflowName}" did not succeed for tag ${tag}. Conclusion was "${workflowConclusions[workflowName].conclusion}". See: ${workflowConclusions[workflowName].html_url}`);
6161
allSuccess = false;
6262
} else {
63-
console.log(` Workflow "${workflowName}" succeeded for tag ${tag}.`);
63+
console.log(`[OK] Workflow "${workflowName}" succeeded for tag ${tag}.`);
6464
}
6565
}
6666

RELEASE-PLAN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ Version referenced below as `X.Y[.Z]` - replace with the real version throughout
3333

3434
- [ ] Unit tests green locally and in CI: `pytest -vv` (should collect `-m 'not release'` by default).
3535
- [ ] `ruff check scenedetect/ tests/` and `ruff format --check scenedetect/ tests/` pass.
36-
- [ ] Release test suite green: tag a disposable `vX.Y.Z-release-rc` or use `workflow_dispatch` on `.github/workflows/release-test.yml` - all 4 jobs (`static`, `release-tests`, `install-matrix`, `long-stress`) green across the 3-OS × 2-Python matrix. See `RELEASE-TEST-PLAN.md` for what the suite covers.
36+
- [ ] Release test suite green: tag a disposable `vX.Y.Z-release-rc` or use `workflow_dispatch` on `.github/workflows/release-test.yml` - all 4 jobs (`static`, `release-tests`, `install-matrix`, `long-stress`) green across the 3-OS x 2-Python matrix. See `RELEASE-TEST-PLAN.md` for what the suite covers.
3737
- [ ] `resources` branch has the artifacts the release tests need (goldens under `tests/resources/goldens/`, `tests/resources/stress_15min.mp4`). Re-push if any golden was regenerated.
3838
- [ ] Manual smoke: fresh venv, `pip install .` (pulls opencv-python automatically) then `pip install .[pyav]`; run `scenedetect -i <video> detect-content list-scenes save-images` and eyeball the output. Repeat after `python packaging/build_headless.py && pip install .` to verify the headless variant.
3939
- [ ] `pip-audit` clean (or exceptions documented in the changelog).
4040

4141
## 5. Windows installer
4242

43-
- [ ] `python scripts/pre_release.py --release` passes (enforces `.aip` `__version__` parity, writes `packaging/windows/.version_info`).
43+
- [ ] `python scripts/pre_release.py --release` passes (enforces `.aip` <-> `__version__` parity, writes `packaging/windows/.version_info`).
4444
- [ ] `pyinstaller packaging/windows/scenedetect.spec` produces a working `scenedetect.exe` - run it against a sample video.
4545
- [ ] `python scripts/stage_windows_dist.py --ffmpeg-dir <dir> --portable-zip` populates `dist/scenedetect/` with ffmpeg, third-party licenses, sphinx docs, and emits the portable `.zip`. Pass `--ffmpeg-dir` pointing at a recent extracted [GyanD codexffmpeg](https://github.com/GyanD/codexffmpeg/releases) build; omit it only for offline builds (uses the bundled `packaging/windows/thirdparty.7z` with a stub `LICENSE-FFMPEG`).
4646
- [ ] `python scripts/bump_installer.py --sync-files` and commit the .aip diff (refreshes the APPDIR baseline so CI's per-build `--sync-only` diff stays small).
@@ -49,7 +49,7 @@ Version referenced below as `X.Y[.Z]` - replace with the real version throughout
4949

5050
> **GUI required for structural changes.** `scripts/bump_installer.py` covers routine version bumps and `--sync-files` covers dependency-driven file-list changes, but anything that touches the *project structure* of the .aip still needs the AdvancedInstaller GUI. Examples:
5151
>
52-
> - Moving the .aip or its source tree (the build's `SourcePath` references are stored relative to the .aip and aren't rewritten by `/NewSync` - cf. the `dist/installer/` `packaging/windows/installer/` move that broke the relative paths until they were edited in the GUI).
52+
> - Moving the .aip or its source tree (the build's `SourcePath` references are stored relative to the .aip and aren't rewritten by `/NewSync` - cf. the `dist/installer/` -> `packaging/windows/installer/` move that broke the relative paths until they were edited in the GUI).
5353
> - Adding/removing build configurations, features, or prerequisites.
5454
> - Editing dialog layouts, branding bitmaps, install sequences, custom actions, file associations, or shortcuts.
5555
> - Changing `UpgradeCode`, install directory layout (`APPDIR` location), or per-component attributes.

appveyor.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ environment:
2020
secure: QRCPoNYF1nqgXDn7pHgBzg==
2121
ai_license_salt:
2222
secure: +Gy+SRk8JUsaM+5pMEKITiJxdLilrxHpkKlrZzR3C9DPwdgYLGxt5sJn6uXuAJg7e6JsKHcT7tRks/HcSKkHPw==
23-
ffmpeg_version: "8.0"
23+
ffmpeg_version: "8.1"
2424

2525
# SignPath Config for Code Signing
2626
deploy:
@@ -42,6 +42,10 @@ install:
4242
- python -m pip install --upgrade -r packaging/windows/requirements.txt --no-binary imageio-ffmpeg
4343
- appveyor DownloadFile https://github.com/GyanD/codexffmpeg/releases/download/%ffmpeg_version%/ffmpeg-%ffmpeg_version%-full_build.7z
4444
- 7z e ffmpeg-%ffmpeg_version%-full_build.7z -odist/ffmpeg ffmpeg.exe LICENSE -r
45+
# moviepy.config reads FFMPEG_BINARY (which routes through imageio_ffmpeg) at import time.
46+
# `--no-binary imageio-ffmpeg` strips the bundled ffmpeg, so point it at the GyanD copy
47+
# we just extracted; otherwise pre_release.py and pyinstaller analysis crash on
48+
# `import scenedetect`. The runtime hook (pyi_rth_scenedetect.py) does the same at exe runtime.
4549
- 'SET IMAGEIO_FFMPEG_EXE=%APPVEYOR_BUILD_FOLDER%\\dist\\ffmpeg\\ffmpeg.exe'
4650

4751
- echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#
2+
# PySceneDetect: Python-Based Video Scene Detector
3+
# ---------------------------------------------------------------
4+
# [ Site: http://www.bcastell.com/projects/PySceneDetect/ ]
5+
# [ Github: https://github.com/Breakthrough/PySceneDetect/ ]
6+
# [ Documentation: http://www.scenedetect.com/docs/ ]
7+
#
8+
# Copyright (C) 2026 Brandon Castellano <http://www.bcastell.com>.
9+
#
10+
# Runtime hook: redirect imageio_ffmpeg and moviepy to the bundled ffmpeg.exe (staged next to
11+
# scenedetect.exe) so we ship a single copy of ffmpeg. Runs before any user imports, which is
12+
# required because moviepy.config reads FFMPEG_BINARY at import time.
13+
14+
15+
def _pyi_rthook():
16+
import os
17+
import sys
18+
19+
bundle_dir = os.path.dirname(sys.executable)
20+
ffmpeg_exe = os.path.join(bundle_dir, "ffmpeg.exe")
21+
if os.path.isfile(ffmpeg_exe):
22+
os.environ["IMAGEIO_FFMPEG_EXE"] = ffmpeg_exe
23+
os.environ.setdefault("FFMPEG_BINARY", ffmpeg_exe)
24+
os.environ["PATH"] = bundle_dir + os.pathsep + os.environ.get("PATH", "")
25+
26+
27+
_pyi_rthook()
28+
del _pyi_rthook

packaging/windows/requirements.txt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# PySceneDetect Requirements for Windows Build
2-
av==14.2.0
3-
click==8.1.8
4-
opencv-python-headless==4.11.0.86
2+
av==17.0.1
3+
click==8.2.1
54
imageio-ffmpeg==0.6.0
6-
moviepy==2.1.2
7-
numpy==2.2.3
8-
platformdirs==4.3.6
9-
tqdm==4.67.1
5+
moviepy==2.2.1
6+
opencv-python-headless==4.13.0.92
7+
numpy==2.4.4
8+
platformdirs==4.9.6
9+
tqdm==4.67.3
1010

1111
# Build-only and test-only requirements.
1212
pyinstaller

packaging/windows/scenedetect.spec

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
# -*- mode: python -*-
22

3+
import os
4+
5+
from PyInstaller.utils.hooks import copy_metadata
6+
37
block_cipher = None
48

9+
# moviepy/imageio resolve their own version via importlib.metadata at import time,
10+
# which needs the dist-info dirs bundled alongside the modules.
11+
_metadata = (
12+
copy_metadata('moviepy')
13+
+ copy_metadata('imageio')
14+
+ copy_metadata('imageio_ffmpeg')
15+
)
16+
517

618
a = Analysis(['../../scenedetect/__main__.py'],
719
pathex=['.'],
@@ -11,15 +23,30 @@ a = Analysis(['../../scenedetect/__main__.py'],
1123
('README.txt', '.'),
1224
('../../LICENSE', '.'),
1325
('../../scenedetect.cfg', '.')
14-
],
15-
hiddenimports=[],
26+
] + _metadata,
27+
hiddenimports=['moviepy', 'imageio', 'imageio_ffmpeg'],
1628
hookspath=[],
17-
runtime_hooks=[],
29+
runtime_hooks=['packaging/windows/pyi_rth_scenedetect.py'],
1830
excludes=[],
1931
win_no_prefer_redirects=False,
2032
win_private_assemblies=False,
2133
cipher=block_cipher)
2234

35+
# Drop imageio_ffmpeg's bundled ffmpeg-*.exe so we don't ship two copies of
36+
# ffmpeg. The runtime hook (pyi_rth_scenedetect.py) redirects imageio_ffmpeg
37+
# and moviepy at the GyanD ffmpeg.exe staged next to scenedetect.exe by
38+
# scripts/stage_windows_dist.py. Keep __init__.py — pyinstaller-hooks-contrib
39+
# declares `imageio_ffmpeg.binaries` as a hidden import, so the package still
40+
# has to be importable.
41+
def _drop_bundled_ffmpeg(toc):
42+
# TOC dest paths use the OS-native separator, so normalize before matching.
43+
prefix = 'imageio_ffmpeg' + os.sep + 'binaries' + os.sep
44+
return [t for t in toc if not (
45+
t[0].startswith(prefix) and not t[0].endswith('__init__.py')
46+
)]
47+
a.binaries = _drop_bundled_ffmpeg(a.binaries)
48+
a.datas = _drop_bundled_ffmpeg(a.datas)
49+
2350
pyz = PYZ(a.pure, a.zipped_data,
2451
cipher=block_cipher)
2552
exe = EXE(pyz,

tests/release/synthetic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
def generate_vfr_swing(output_path: str):
2121
"""Generates a VFR video with three segments separated by visible luma steps.
2222
23-
Segments: black @ 1 fps (5s) gray @ 60 fps (5s) white @ 1 fps (5s).
23+
Segments: black @ 1 fps (5s) -> gray @ 60 fps (5s) -> white @ 1 fps (5s).
2424
Solid colors make the cuts unambiguous for ContentDetector; mixed rates
2525
exercise the VFR code path. Boundary timestamps: 5.0s and 10.0s.
2626
"""

tests/release/test_backends.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def test_cross_backend_consistency(rel_path, is_vfr):
7979
)
8080
if is_vfr:
8181
for a, e in zip(actual, expected, strict=True):
82-
# Tolerance: ~one frame at 30 fps. Plan calls for ±1 local-frame-duration;
82+
# Tolerance: ~one frame at 30 fps. Plan calls for +/-1 local-frame-duration;
8383
# 50 ms is a conservative superset that still catches real drift.
8484
assert abs(a - e) < 0.05, (
8585
f"VFR timestamp drift between {backend} and {reference}: {a} vs {e}"

tests/release/test_long_video.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def monitor_memory():
7070
stop_event.set()
7171
monitor_thread.join()
7272

73-
# Assert peak RSS 3x baseline
73+
# Assert peak RSS <= 3x baseline
7474
# Some increase is expected due to internal buffering, but not 3x for 480p.
7575
assert peak_rss[0] <= 3 * baseline_rss, (
7676
f"Memory leak suspected: Peak RSS {peak_rss[0]} > 3x Baseline RSS {baseline_rss}"

0 commit comments

Comments
 (0)