Skip to content

Commit c1fcb79

Browse files
committed
[dist] Split scenedetect into scenedetect-core #558
1 parent 0abfbc4 commit c1fcb79

18 files changed

Lines changed: 429 additions & 75 deletions

.github/workflows/build.yml

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ jobs:
7373
python -m scenedetect version
7474
python -m scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s
7575
python -m scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s
76-
python -m pip uninstall -y scenedetect
76+
python -m pip uninstall -y scenedetect-core
7777
7878
- name: Build Package
7979
shell: bash
8080
run: |
81-
python -m build
81+
# Builds scenedetect-core plus the scenedetect/scenedetect-headless variants.
82+
python packaging/build_all.py
8283
echo "scenedetect_version=`python -c \"import scenedetect; print(scenedetect.__version__.replace('-', '.'))\"`" >> "$GITHUB_ENV"
8384
8485
- name: Smoke Test Package (Source Dist)
@@ -89,6 +90,7 @@ jobs:
8990
[ -d .smoke-sdist/Scripts ] && VENV_BIN=.smoke-sdist/Scripts
9091
source "$VENV_BIN/activate"
9192
pip install "dist/scenedetect-${{ env.scenedetect_version }}.tar.gz[pyav]" --only-binary av
93+
python -c "import importlib.metadata as m; ds = sorted(d.metadata['Name'] for d in m.distributions() if d.metadata['Name'] in ('opencv-python', 'opencv-python-headless')); assert ds == ['opencv-python'], f'Expected only opencv-python, got: {ds}'"
9294
scenedetect version
9395
scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s
9496
scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s
@@ -105,13 +107,6 @@ jobs:
105107
scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s
106108
scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s
107109
108-
- name: Build Package (Headless)
109-
shell: bash
110-
run: |
111-
python packaging/build_headless.py
112-
python -m build
113-
git checkout pyproject.toml
114-
115110
- name: Smoke Test Package (Headless Wheel)
116111
shell: bash
117112
run: |
@@ -127,6 +122,49 @@ jobs:
127122
scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s
128123
scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s
129124
125+
- name: Smoke Test Package (Core Only)
126+
shell: bash
127+
run: |
128+
python -m venv .smoke-core
129+
VENV_BIN=.smoke-core/bin
130+
[ -d .smoke-core/Scripts ] && VENV_BIN=.smoke-core/Scripts
131+
source "$VENV_BIN/activate"
132+
pip install "dist/scenedetect_core-${{ env.scenedetect_version }}-py3-none-any.whl"
133+
# Core declares no OpenCV variant; importing without one must raise the friendly error.
134+
python -c "
135+
try:
136+
import scenedetect
137+
raise AssertionError('import should fail without cv2')
138+
except ModuleNotFoundError as ex:
139+
assert ex.name == 'cv2', ex
140+
"
141+
# Any variant satisfies the library; core must not drag in CLI deps or the entry point.
142+
pip install opencv-python-headless
143+
python -c "import scenedetect; print(scenedetect.__version__)"
144+
python -c "import importlib.util as u; assert u.find_spec('click') is None, 'click must not be a core dependency'"
145+
python -c "import importlib.util as u; assert u.find_spec('tqdm') is None, 'tqdm must not be a core dependency'"
146+
if command -v scenedetect >/dev/null 2>&1; then
147+
echo "The scenedetect entry point must not be installed by scenedetect-core"
148+
exit 1
149+
fi
150+
151+
- name: Smoke Test Package (Upgrade From 0.7)
152+
shell: bash
153+
run: |
154+
# All three packages ship the code (no metapackage layering) precisely so that
155+
# in-place upgrades from pre-0.7.1 installs keep working - a code-carrying dist
156+
# flipped to a code-free metapackage would break here, since uninstalling the old
157+
# version deletes module files a dependency just wrote. Keep this as a regression
158+
# guard for that failure mode (https://scenedetect.com/issues/558).
159+
python -m venv .smoke-upgrade
160+
VENV_BIN=.smoke-upgrade/bin
161+
[ -d .smoke-upgrade/Scripts ] && VENV_BIN=.smoke-upgrade/Scripts
162+
source "$VENV_BIN/activate"
163+
pip install "scenedetect==0.7"
164+
pip install --upgrade --find-links dist/ "scenedetect==${{ env.scenedetect_version }}"
165+
python -c "import scenedetect; print(scenedetect.__version__)"
166+
scenedetect version
167+
130168
- name: Upload Package
131169
if: ${{ matrix.python-version == '3.13' && matrix.os == 'ubuntu-latest' }}
132170
uses: actions/upload-artifact@v6

.github/workflows/publish-pypi.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ jobs:
103103
run-id: ${{ steps.resolve.outputs.run-id }}
104104

105105
- name: List artifact contents
106+
# Expect 6 files: sdist + wheel for each of scenedetect-core, scenedetect,
107+
# and scenedetect-headless. All three projects publish from this one step;
108+
# each needs a trusted publisher configured on PyPI/TestPyPI for this workflow.
106109
run: ls -la pkg/
107110

108111
- name: Publish Package
@@ -111,5 +114,5 @@ jobs:
111114
repository-url: ${{ github.event.inputs.environment == 'testpypi' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
112115
packages-dir: pkg/
113116
print-hash: true
114-
# Tolerate retries: skip existing packages if for example only one variant was uploaded.
117+
# Tolerate retries: skip existing packages if for example only some variants were uploaded.
115118
skip-existing: true

.github/workflows/release-test.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ jobs:
4444
fi
4545
- name: Build and Check
4646
run: |
47-
python -m build
48-
python packaging/build_headless.py
49-
python -m build
50-
git checkout pyproject.toml
51-
twine check dist/*
47+
# Builds scenedetect-core plus the scenedetect/scenedetect-headless variants.
48+
python packaging/build_all.py
49+
# Glob by extension: dist/ also holds tracked website assets (dist/logo/),
50+
# which `twine check dist/*` would reject as an unknown distribution.
51+
twine check dist/*.whl dist/*.tar.gz
5252
- name: pip-audit
5353
# CVE-2026-3219 in pip 26.0.1 has no fix version available upstream
5454
# and pip ships pre-installed on the runner (not controlled by this
@@ -79,7 +79,9 @@ jobs:
7979
- name: Install dependencies
8080
run: |
8181
python -m pip install --upgrade pip
82-
pip install .[pyav,moviepy]
82+
# The dev extra supplies the CLI deps (click/opencv/tqdm) plus av and moviepy;
83+
# a bare `pip install .` is now scenedetect-core with numpy only.
84+
pip install .[dev]
8385
pip install opentimelineio pillow psutil pytest
8486
- name: Run release tests
8587
run: pytest -m release -vv --ignore=tests/release/test_long_video_stress.py
@@ -104,7 +106,8 @@ jobs:
104106
- name: Install dependencies
105107
run: |
106108
python -m pip install --upgrade pip
107-
pip install .[pyav]
109+
# The dev extra supplies the CLI deps (click/opencv/tqdm) plus av.
110+
pip install .[dev]
108111
pip install psutil pytest
109112
- name: Run long stress test
110113
run: pytest -m release -k long_video -vv

.gitignore

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ tests/resources/*
1111
*.mkv
1212
*.m4v
1313
*.csv
14-
packaging/windows/.version_info
15-
packaging/windows/installer/PySceneDetect.back*.aip
16-
packaging/windows/installer/PySceneDetect-*.msi
17-
packaging/windows/installer/PySceneDetect-cache/
1814
*.txt
1915

2016
benchmark/BBC/
2117
benchmark/AutoShot/
2218
benchmark/ClipShots/
2319
benchmark/results/
2420

21+
packaging/windows/.version_info
22+
packaging/windows/installer/PySceneDetect.back*.aip
23+
packaging/windows/installer/PySceneDetect-*.msi
24+
packaging/windows/installer/PySceneDetect-cache/
25+
2526
# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
2627

2728
__pycache__/

RELEASE-PLAN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ Optional: version referenced below as `X.Y[.Z]` - replace with the real version
5454
## 6. Publish & Release Checks
5555

5656
- [ ] Publish Github release
57-
- [ ] Upload to PyPI: `publish-pypi.yml` must be manually triggered on a release tag. Specify `testpypi` first, and make sure everything goes okay on the test instance. When verified and smoke tested, specify `pypi` as the environment, and publish the production package.
58-
- [ ] Verify both projects: https://pypi.org/project/scenedetect/ and https://pypi.org/project/scenedetect-headless/.
57+
- [ ] Upload to PyPI: `publish-pypi.yml` must be manually triggered on a release tag. Specify `testpypi` first, and make sure everything goes okay on the test instance. When verified and smoke tested, specify `pypi` as the environment, and publish the production package. The artifact contains 6 files (sdist + wheel for `scenedetect-core`, `scenedetect`, and `scenedetect-headless`).
58+
- [ ] Verify all three projects: https://pypi.org/project/scenedetect/, https://pypi.org/project/scenedetect-headless/, and https://pypi.org/project/scenedetect-core/.
5959
- [ ] Deploy website: `generate-website.yml`
6060
- [ ] Deploy docs: `generate-docs.yml`
6161
- [ ] Merge release branch back into `main`, verify `docs/LATEST_VERSION` is correct

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ PySceneDetect Documentation
88

99
Welcome to the PySceneDetect docs. The docs are split into two separate parts: one for the command-line interface (the `scenedetect` command) and another for the Python API (the `scenedetect` module).
1010

11-
You can install the latest release of PySceneDetect by running `pip install scenedetect` (or `pip install scenedetect-headless` on servers without GUI libraries), or by downloading the Windows build from `scenedetect.com/download <http://www.scenedetect.com/download/>`_. PySceneDetect requires `ffmpeg` or `mkvmerge` for video splitting support.
11+
You can install the latest release of PySceneDetect by running `pip install scenedetect` (or `pip install scenedetect-headless` on servers without GUI libraries), or by downloading the Windows build from `scenedetect.com/download <http://www.scenedetect.com/download/>`_. Library-only installs that need a specific OpenCV variant (e.g. `opencv-contrib-python`) can use `pip install scenedetect-core`, which has no CLI dependencies and lets you supply any OpenCV package. PySceneDetect requires `ffmpeg` or `mkvmerge` for video splitting support.
1212

1313
.. note::
1414

packaging/build_all.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
"""Builds all three PySceneDetect distributions into dist/:
11+
12+
- scenedetect-core: minimal dependencies (numpy only, no OpenCV variant declared,
13+
no console script) - built from the repo root pyproject.toml
14+
- scenedetect / scenedetect-headless: the same code plus an OpenCV variant, the
15+
CLI dependencies, and the `scenedetect` console script
16+
17+
All three are code-carrying packages built from the repo root, so they share the
18+
same source, readme, and dynamic version. The scenedetect / scenedetect-headless
19+
variants are produced by temporarily swapping packaging/variants/pyproject-<name>.toml
20+
into the repo root (restored afterwards, even on failure).
21+
22+
Requires `build` (pip install build). Fails if dist/ ends up with any wheel/sdist
23+
besides the six expected artifacts, so clear stale build artifacts from dist/ first.
24+
(Other dist/ contents are ignored - e.g. dist/logo/ is tracked website assets.)
25+
"""
26+
27+
import ast
28+
import subprocess
29+
import sys
30+
from pathlib import Path
31+
32+
ROOT = Path(__file__).resolve().parent.parent
33+
DIST = ROOT / "dist"
34+
PYPROJECT = ROOT / "pyproject.toml"
35+
VARIANTS = ("scenedetect", "scenedetect-headless")
36+
37+
38+
def get_version() -> str:
39+
"""Parse scenedetect.__version__ without importing (avoids the cv2 guard),
40+
normalized per PEP 440 (e.g. 0.7.1-dev0 -> 0.7.1.dev0)."""
41+
source = (ROOT / "scenedetect" / "__init__.py").read_text(encoding="utf-8")
42+
for node in ast.parse(source).body:
43+
if isinstance(node, ast.Assign) and any(
44+
getattr(target, "id", None) == "__version__" for target in node.targets
45+
):
46+
assert isinstance(node.value, ast.Constant)
47+
return str(node.value.value).replace("-", ".")
48+
raise SystemExit("Could not find __version__ in scenedetect/__init__.py")
49+
50+
51+
def build() -> None:
52+
subprocess.check_call([sys.executable, "-m", "build", "--outdir", str(DIST), str(ROOT)])
53+
54+
55+
def main() -> None:
56+
version = get_version()
57+
58+
original = PYPROJECT.read_text(encoding="utf-8")
59+
if 'name = "scenedetect-core"' not in original:
60+
raise SystemExit(
61+
"pyproject.toml is not the scenedetect-core baseline - likely left over "
62+
"from an interrupted build. Restore it (e.g. `git checkout pyproject.toml`) "
63+
"and re-run."
64+
)
65+
66+
build() # scenedetect-core from the unmodified repo root.
67+
try:
68+
for name in VARIANTS:
69+
variant = (ROOT / "packaging" / "variants" / f"pyproject-{name}.toml").read_text(
70+
encoding="utf-8"
71+
)
72+
assert f'name = "{name}"' in variant, f"unexpected package name in variant {name}"
73+
PYPROJECT.write_text(variant, encoding="utf-8")
74+
build()
75+
finally:
76+
PYPROJECT.write_text(original, encoding="utf-8")
77+
78+
expected = set()
79+
for name in ("scenedetect-core", *VARIANTS):
80+
normalized = name.replace("-", "_")
81+
expected.add(f"{normalized}-{version}.tar.gz")
82+
expected.add(f"{normalized}-{version}-py3-none-any.whl")
83+
# Only validate build artifacts: dist/ also holds tracked files (e.g. dist/logo/).
84+
actual = {
85+
path.name
86+
for path in DIST.iterdir()
87+
if path.is_file() and (path.name.endswith(".whl") or path.name.endswith(".tar.gz"))
88+
}
89+
if actual != expected:
90+
raise SystemExit(
91+
f"dist/ mismatch (stale files or failed build?)\n"
92+
f" missing: {sorted(expected - actual)}\n"
93+
f" unexpected: {sorted(actual - expected)}"
94+
)
95+
print(f"Built {len(expected)} artifacts for version {version}:")
96+
for filename in sorted(expected):
97+
print(f" dist/{filename}")
98+
99+
100+
if __name__ == "__main__":
101+
main()

packaging/build_headless.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

packaging/package-info.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Github Repo: https://github.com/Breakthrough/PySceneDetect/
2525

2626
Install: ``pip install --upgrade scenedetect`` (or ``scenedetect-headless`` for servers)
2727

28+
Packages: `scenedetect <https://pypi.org/project/scenedetect/>`_ (CLI + ``opencv-python``), `scenedetect-headless <https://pypi.org/project/scenedetect-headless/>`_ (CLI + ``opencv-python-headless``), and `scenedetect-core <https://pypi.org/project/scenedetect-core/>`_ (library only, minimal dependencies, bring your own OpenCV variant). All provide the same ``scenedetect`` module -- install only one.
29+
2830
----------------------------------------------------------
2931

3032
**PySceneDetect** is a tool for detecting shot changes in videos, and can automatically split videos into separate clips. PySceneDetect is free and open-source software, and has several detection methods to find fast-cuts and threshold-based fades.

0 commit comments

Comments
 (0)