Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ jobs:
python-version: ["3.13"]

env:
ffmpeg-version: "7.1"
IMAGEIO_FFMPEG_EXE: ""
ffmpeg-version: "8.1"

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

- name: Build PySceneDetect
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
core.setFailed(`Workflow "${workflowName}" did not succeed for tag ${tag}. Conclusion was "${workflowConclusions[workflowName].conclusion}". See: ${workflowConclusions[workflowName].html_url}`);
allSuccess = false;
} else {
console.log(` Workflow "${workflowName}" succeeded for tag ${tag}.`);
console.log(`[OK] Workflow "${workflowName}" succeeded for tag ${tag}.`);
}
}

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
docs/_build/
website/build/
scripts/local/
tests/resources/*
*.mp4
*.jpg
Expand All @@ -10,6 +11,7 @@ tests/resources/*
*.m4v
*.csv
packaging/windows/.version_info
packaging/windows/installer/PySceneDetect.back*.aip
benchmarks/BCC/*.mp4
*.txt
benchmarks/RAI/*.mp4
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ Video Cut Detection and Analysis Tool

**Quick Install**:

pip install scenedetect[opencv] --upgrade
pip install scenedetect --upgrade # standard (depends on opencv-python)
pip install scenedetect-headless --upgrade # headless servers (depends on opencv-python-headless)

Requires ffmpeg/mkvmerge for video splitting support. Windows builds (MSI installer/portable ZIP) can be found on [the download page](https://scenedetect.com/download/).

Expand Down
23 changes: 16 additions & 7 deletions RELEASE-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ Version referenced below as `X.Y[.Z]` - replace with the real version throughout
## 1. Code & version

- [ ] Bump `__version__` in `scenedetect/__init__.py`.
- [ ] Bump `ProductVersion` in `packaging/windows/installer/PySceneDetect.aip` (must match `__version__` - `scripts/pre_release.py --release` asserts this).
- [ ] No `-dev` / pre-release suffix on the version string for a final release.

> **Note:** `setup.cfg` reads the package version dynamically via `version = attr: scenedetect.__version__`, and `pyproject.toml` does not declare a `version` field. The single source of truth is `scenedetect/__init__.py`; the `.aip` is the only other place to keep in sync.
- [ ] Regular release: No `-dev` suffix or other, pre-release: has `-dev0`, `-dev1`, ...

## 2. Docs

Expand All @@ -33,16 +30,28 @@ Version referenced below as `X.Y[.Z]` - replace with the real version throughout

- [ ] Unit tests green locally and in CI: `pytest -vv` (should collect `-m 'not release'` by default).
- [ ] `ruff check scenedetect/ tests/` and `ruff format --check scenedetect/ tests/` pass.
- [ ] 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.
- [ ] 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.
- [ ] `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.
- [ ] 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.
- [ ] `pip-audit` clean (or exceptions documented in the changelog).

## 5. Windows installer

- [ ] `python scripts/pre_release.py --release` passes (enforces `.aip` `__version__` parity, writes `packaging/windows/.version_info`).
- [ ] `python scripts/pre_release.py --release` passes (enforces `.aip` <-> `__version__` parity, writes `packaging/windows/.version_info`).
- [ ] `pyinstaller packaging/windows/scenedetect.spec` produces a working `scenedetect.exe` - run it against a sample video.
- [ ] `python scripts/stage_windows_dist.py --ffmpeg-dir <dir>` 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`).
- [ ] `python scripts/update_installer.py --sync-files` and commit the .aip diff (refreshes the APPDIR baseline so CI's per-build `--sync-only` diff stays small).
- [ ] Build the MSI via Advanced Installer (`packaging/windows/installer/PySceneDetect.aip`); install into a clean Windows VM and run the CLI.
- [ ] After both `pyinstaller` and the MSI build are done (and the portable `.zip` is staged at `dist/PySceneDetect-X.Y.Z-portable.zip`), run `python scripts/generate_manifest.py` to produce `dist/PySceneDetect-X.Y.Z.manifest.json` (per-file SHA256 audit of every artifact) and `dist/SHA256SUMS` (flat `sha256sum -c` compatible). Both are attached to the GitHub release in step 7.

> **GUI required for structural changes.** `scripts/update_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:
>
> - 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).
> - Adding/removing build configurations, features, or prerequisites.
> - Editing dialog layouts, branding bitmaps, install sequences, custom actions, file associations, or shortcuts.
> - Changing `UpgradeCode`, install directory layout (`APPDIR` location), or per-component attributes.
>
> When in doubt, open the .aip in AdvancedInstaller, make the change, save, and commit the resulting diff. Re-run `update_installer.py` afterwards if the version-identity fields need refreshing.

## 6. Cut the release

Expand All @@ -55,7 +64,7 @@ Version referenced below as `X.Y[.Z]` - replace with the real version throughout

- [ ] `publish-pypi.yml` ran on the tag and uploaded successfully. Verify both projects: https://pypi.org/project/scenedetect/ and https://pypi.org/project/scenedetect-headless/.
- [ ] Smoke-test PyPI: in a fresh venv, `pip install scenedetect==X.Y.Z`; CLI launches and `pip show scenedetect` lists `opencv-python`. Repeat in a second venv with `pip install scenedetect-headless==X.Y.Z`; verify it lists `opencv-python-headless`.
- [ ] Create GitHub Release from the `vX.Y[.Z]` tag, body = changelog section, attach Windows installer MSI + portable `.zip`.
- [ ] Create GitHub Release from the `vX.Y[.Z]` tag, body = changelog section, attach Windows installer MSI + portable `.zip` + `PySceneDetect-X.Y.Z.manifest.json` + `SHA256SUMS` (both produced by `scripts/generate_manifest.py`).
- [ ] Deploy website: `generate-website.yml` picks up the changelog / download page updates.
- [ ] Deploy docs: `generate-docs.yml` publishes the new version.

Expand Down
77 changes: 53 additions & 24 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

build: false

cache:
- 'ffmpeg-%ffmpeg_version%-full_build.7z -> appveyor.yml'
- 'packaging\windows\installer\advinst.msi -> appveyor.yml'
- '%LOCALAPPDATA%\uv\cache -> pyproject.toml'
- 'C:\Program Files\Inkscape -> appveyor.yml'

# Branches applies to tags as well. We only build on tagged releases of the form vX.Y.Z-release
branches:
only:
Expand All @@ -20,7 +26,7 @@ environment:
secure: QRCPoNYF1nqgXDn7pHgBzg==
ai_license_salt:
secure: +Gy+SRk8JUsaM+5pMEKITiJxdLilrxHpkKlrZzR3C9DPwdgYLGxt5sJn6uXuAJg7e6JsKHcT7tRks/HcSKkHPw==
ffmpeg_version: "8.0"
ffmpeg_version: "8.1"

# SignPath Config for Code Signing
deploy:
Expand All @@ -37,31 +43,33 @@ install:
- echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- 'SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%'
- python --version
- python -m pip install --upgrade pip build wheel virtualenv setuptools
- python -m pip install .[docs]
- python -m pip install --upgrade -r packaging/windows/requirements.txt --no-binary imageio-ffmpeg
# Checkout build resources and third party software used for testing.
- git checkout refs/remotes/origin/resources -- packaging/
- appveyor DownloadFile https://github.com/GyanD/codexffmpeg/releases/download/%ffmpeg_version%/ffmpeg-%ffmpeg_version%-full_build.7z
- python -m pip install uv
- uv pip install --system .[docs]
- uv pip install --system -r packaging/windows/requirements.txt --no-binary imageio-ffmpeg
- if not exist ffmpeg-%ffmpeg_version%-full_build.7z appveyor DownloadFile https://github.com/GyanD/codexffmpeg/releases/download/%ffmpeg_version%/ffmpeg-%ffmpeg_version%-full_build.7z
- 7z e ffmpeg-%ffmpeg_version%-full_build.7z -odist/ffmpeg ffmpeg.exe LICENSE -r
# moviepy.config reads FFMPEG_BINARY (which routes through imageio_ffmpeg) at import time.
# `--no-binary imageio-ffmpeg` strips the bundled ffmpeg, so point it at the GyanD copy
# we just extracted; otherwise pre_release.py and pyinstaller analysis crash on
# `import scenedetect`. The runtime hook (pyi_rth_scenedetect.py) does the same at exe runtime.
- 'SET IMAGEIO_FFMPEG_EXE=%APPVEYOR_BUILD_FOLDER%\\dist\\ffmpeg\\ffmpeg.exe'
# Inkscape is required by scripts/pre_release.py --release (regenerates installer JPGs
# from the master SVG). Not preinstalled on the AppVeyor VS2019 image; cached
# in `C:\Program Files\Inkscape` (see cache: section) so we only re-install when
# the cache is busted (appveyor.yml changes).
- if not exist "C:\Program Files\Inkscape\bin\inkscape.exe" choco install inkscape -y --no-progress
- 'SET PATH=%PATH%;C:\Program Files\Inkscape\bin'

- echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- echo * * BUILDING WINDOWS EXE * *
- echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Build Windows .EXE and create portable .ZIP
# Build Windows .EXE and create portable .ZIP. The staging script copies
# ffmpeg.exe + LICENSE from --ffmpeg-dir, third-party licenses, the project
# LICENSE/README, and sphinx docs into dist/scenedetect/, then emits the
# portable .zip - keeps CI and local builds in sync (see scripts/stage_windows_dist.py).
- python scripts/pre_release.py --release
- pyinstaller packaging/windows/scenedetect.spec
- sphinx-build -b singlehtml docs dist/scenedetect/docs
- mkdir dist\scenedetect\thirdparty
- move LICENSE dist\scenedetect\
- copy packaging\windows\LICENSE-PYTHON dist\scenedetect\thirdparty\
- copy scenedetect\_thirdparty\LICENSE* dist\scenedetect\thirdparty\
- copy dist\ffmpeg\ffmpeg.exe dist\scenedetect\
- move dist\ffmpeg\LICENSE dist\scenedetect\thirdparty\LICENSE-FFMPEG
- cd dist/scenedetect
- 7z a ../scenedetect-win64.zip *
- cd ../..
- python scripts/stage_windows_dist.py --ffmpeg-dir dist/ffmpeg

- echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- echo * * BUILDING MSI INSTALLER * *
Expand All @@ -70,14 +78,30 @@ install:
- cd packaging/windows/installer
- ps: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/secure-file/master/install.ps1'))
- appveyor-tools\secure-file -decrypt license65.dat.enc -secret %ai_license_secret% -salt %ai_license_salt%
- appveyor DownloadFile https://www.advancedinstaller.com/downloads/advinst.msi
- if not exist advinst.msi appveyor DownloadFile https://www.advancedinstaller.com/downloads/advinst.msi
- msiexec /i advinst.msi /qn
- 'SET PATH=%PATH%;C:\\Program Files (x86)\\Caphyon\\Advanced Installer 22.9.1\\bin\\x86'
# Resolve the installed Advanced Installer bin path dynamically - the upstream
# MSI is unversioned so the directory name (Advanced Installer X.Y.Z) drifts.
- ps: $aiBin = (Get-ChildItem 'C:\Program Files (x86)\Caphyon\Advanced Installer*\bin\x86' | Sort-Object FullName -Descending | Select-Object -First 1).FullName; Add-Content $env:APPVEYOR_BUILD_FOLDER\ai_path.txt $aiBin
- set /p AI_BIN=<%APPVEYOR_BUILD_FOLDER%\ai_path.txt
- 'SET PATH=%PATH%;%AI_BIN%'
# License path must be absolute
- AdvancedInstaller.com /RegisterOffline "%cd%\license65.dat"
- cd ../../..
# Re-sync APPDIR from CI's dist/scenedetect (handles drift between local and
# CI pyinstaller output - new transitive deps, Python patch updates, etc.).
# Does not touch version/GUID fields - those are committed to the .aip on the
# release tag and must stay stable across rebuilds for upgrade-chain integrity.
# On non-tag builds, also pass --dev so the MSI is named PySceneDetect-{ver}-dev-win64.msi
# (keeps dev artifacts distinguishable from signed releases).
- if "%APPVEYOR_REPO_TAG%"=="true" (python scripts/update_installer.py --sync-only) else (python scripts/update_installer.py --sync-only --dev)
# Snapshot the post-sync .aip and the actual payload tree as build artifacts.
# The committed .aip is a baseline; CI adapts it to its own pyinstaller output
# and we never write back to git, so these snapshots are the authoritative
# record of what each MSI was built from (for audit / release forensics).
- copy packaging\windows\installer\PySceneDetect.aip dist\PySceneDetect.aip
# Create MSI installer
- AdvancedInstaller.com /build PySceneDetect.aip
- cd ../..
- AdvancedInstaller.com /build packaging/windows/installer/PySceneDetect.aip

- echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- echo * * PACKAGING BUILD ARTIFACTS * *
Expand Down Expand Up @@ -109,9 +133,14 @@ test_script:
- scenedetect.exe -i ../../tests/resources/testvideo.mp4 -b pyav detect-content time -e 2s

artifacts:
# Portable ZIP
- path: dist/scenedetect-win64.zip
# Portable ZIP (named PySceneDetect-X.Y.Z-portable.zip by stage_windows_dist.py)
- path: dist/PySceneDetect-*-portable.zip
name: PySceneDetect-win64_portable
# MSI Installer + .EXE Bundle for Signing
- path: dist/scenedetect-signed.zip
name: PySceneDetect-win64_installer
# Build provenance: post-sync .aip and the portable payload manifest.
- path: dist/PySceneDetect.aip
name: PySceneDetect-build-manifest-aip
- path: dist/PySceneDetect-*-portable.manifest.txt
name: PySceneDetect-build-manifest-payload
Binary file modified docs/_static/favicon.ico
Binary file not shown.
Loading
Loading