Skip to content

Commit 5d5bcac

Browse files
committed
[dist] Update Windows dependencies for 0.7 release
1 parent 217de5d commit 5d5bcac

17 files changed

Lines changed: 106 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)