diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index a6f82cd7..00000000 --- a/.dockerignore +++ /dev/null @@ -1,71 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2014-2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# - -# Version control -.git -.gitignore -.github - -# Python / Build artifacts -__pycache__/ -*.pyc -*.pyo -*.pyd -.Python -env/ -venv/ -.venv/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# Testing / Tooling -.pytest_cache/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.log -.mypy_cache/ -.hypothesis/ - -# IDE / Project files -.vscode/ -.idea/ -*.swp -*.swo -.DS_Store - -# Project specific exclusions (to keep the image lean) -test_clips/ -tests/ -docs/ -website/ -benchmark/ -scenedetect.cfg - diff --git a/.github/ISSUE_TEMPLATE/scenedetect_app.md b/.github/ISSUE_TEMPLATE/scenedetect_app.md deleted file mode 100644 index 49658a85..00000000 --- a/.github/ISSUE_TEMPLATE/scenedetect_app.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Application Bug -about: Help us fix and improve PySceneDetect by reporting bugs or other issues. - ---- - -**Description:** - -Describe what the bug or issue is (e.g. crashes when setting X) and how it can be reproduced. - -**Command:** - -Place a full copy of the command line options you are using here, for example: - -`scenedetect -i some_video.mp4 -s some_video.stats.csv -o outdir detect-content --threshold 28 list-scenes save-images` - -**Output:** - -Copy the output of running the application here. Where possible, generate a debug log by adding `-v debug -l BUG_REPORT.txt` to the beginning of your command, and **attach `BUG_REPORT.txt` to your issue**. - -**Environment:** - -The operating system and how you installed PySceneDetect may be relevant to the issue. Please run `scenedetect version` and copy the output here, or provide other details on how PySceneDetect was installed. - -**Media/Files:** - -Attach or link to any files relevant to the issue, including videos (or YouTube links), scene files, stats files, and log output. diff --git a/.github/ISSUE_TEMPLATE/scenedetect_package.md b/.github/ISSUE_TEMPLATE/scenedetect_package.md deleted file mode 100644 index 7fbca92c..00000000 --- a/.github/ISSUE_TEMPLATE/scenedetect_package.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Python API -about: Programmers using the `scenedetect` package. - ---- - -**Description:** - -Describe the issue (unexpected result, exception thrown, etc...) and any relevant output. - -**Example:** - -Include code samples that demonstrate the issue: - -```python -from scenedetect import detect, ContentDetector, split_video_ffmpeg -scene_list = detect('my_video.mp4', ContentDetector()) -split_video_ffmpeg('my_video.mp4', scene_list) -``` - -**Environment:** - -Run `scenedetect version` and include the output. This will describe the environment/OS/platform and versions of dependencies you have installed. - -**Media/Files:** - -Attach or link to any files relevant to the issue, including videos (or YouTube links), scene files, stats files, and log output. diff --git a/.github/ISSUE_TEMPLATE/scenedetect_request.md b/.github/ISSUE_TEMPLATE/scenedetect_request.md deleted file mode 100644 index 0de1ff8e..00000000 --- a/.github/ISSUE_TEMPLATE/scenedetect_request.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Feature or Enhancement -about: Submit an idea or feature request to make PySceneDetect better. - ---- - -**Problem/Use Case** - -Describe what problem you want to solve, or what use case you want to achieve. Ex. PySceneDetect doesn't work well in cases X or Y but if it could detect Z [...], PySceneDetect is slow because of X but could be faster if it did Y [...], or I need PySceneDetect to do X because of some condition Y [...]. - -**Solutions** - -Discuss any potential solutions here. - -**Proposed Implementation:** - -Description of what you want to happen, and how you think the feature/enhancement should be implemented. Ex. what command line options/arguments should be added to PySceneDetect, and examples of how you expect them to function. - -**Alternatives:** - -List any alternative solutions or related ideas you've considered. - -**Examples:** - -Attach or link to any relevant videos or images that are relevant. diff --git a/.github/actions/setup-ffmpeg/action.yml b/.github/actions/setup-ffmpeg/action.yml deleted file mode 100644 index 76cee703..00000000 --- a/.github/actions/setup-ffmpeg/action.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: 'Setup FFmpeg' -description: 'Ensure ffmpeg is available on the runner, using OS package managers as a fallback.' -inputs: - github-token: - description: 'Unused; kept for backward compatibility with existing callers.' - required: false - default: '' - -runs: - using: 'composite' - steps: - - name: Set ffmpeg install config - shell: bash - run: | - VERSION=8.1 - echo "FFMPEG_VERSION=${VERSION}" >> "$GITHUB_ENV" - echo "FFMPEG_ASSET=ffmpeg-${VERSION}-essentials_build.zip" >> "$GITHUB_ENV" - - - name: Check for preinstalled ffmpeg - id: check - shell: bash - run: | - if command -v ffmpeg >/dev/null 2>&1; then - echo "ffmpeg already available at: $(command -v ffmpeg)" - ffmpeg -version | head -n 1 - echo "installed=true" >> "$GITHUB_OUTPUT" - else - echo "ffmpeg not found on PATH; will install via package manager." - echo "installed=false" >> "$GITHUB_OUTPUT" - fi - - - name: Install ffmpeg (Linux) - if: ${{ steps.check.outputs.installed == 'false' && runner.os == 'Linux' }} - shell: bash - run: | - attempts=4 - for attempt in $(seq 1 $attempts); do - echo "apt-get attempt $attempt" - if sudo apt-get update && sudo apt-get install -y ffmpeg; then - exit 0 - fi - if [ "$attempt" -lt "$attempts" ]; then - delay=$(( 15 * (1 << (attempt - 1)) + RANDOM % 6 )) - echo "install failed; sleeping ${delay}s before retry" - sleep "$delay" - fi - done - echo "Failed to install ffmpeg via apt-get after $attempts attempts" >&2 - exit 1 - - - name: Install ffmpeg (macOS) - if: ${{ steps.check.outputs.installed == 'false' && runner.os == 'macOS' }} - shell: bash - run: | - attempts=4 - for attempt in $(seq 1 $attempts); do - echo "brew attempt $attempt" - if brew install ffmpeg; then - exit 0 - fi - if [ "$attempt" -lt "$attempts" ]; then - delay=$(( 15 * (1 << (attempt - 1)) + RANDOM % 6 )) - echo "install failed; sleeping ${delay}s before retry" - sleep "$delay" - fi - done - echo "Failed to install ffmpeg via brew after $attempts attempts" >&2 - exit 1 - - - name: Cache ffmpeg (Windows) - if: ${{ steps.check.outputs.installed == 'false' && runner.os == 'Windows' }} - id: cache-ffmpeg-windows - uses: actions/cache@v4 - with: - path: C:\ffmpeg-bin - key: ffmpeg-windows-${{ env.FFMPEG_VERSION }}-v1 - - - name: Install ffmpeg (Windows) - if: ${{ steps.check.outputs.installed == 'false' && runner.os == 'Windows' && steps.cache-ffmpeg-windows.outputs.cache-hit != 'true' }} - shell: pwsh - env: - GH_TOKEN: ${{ github.token }} - run: | - $attempts = 4 - $downloaded = $false - for ($attempt = 1; $attempt -le $attempts; $attempt++) { - Write-Host "download attempt $attempt (GyanD/codexffmpeg $env:FFMPEG_VERSION)" - gh release download $env:FFMPEG_VERSION --repo GyanD/codexffmpeg --pattern $env:FFMPEG_ASSET --clobber - if ($LASTEXITCODE -eq 0) { $downloaded = $true; break } - if ($attempt -lt $attempts) { - $delay = [int]([math]::Pow(2, $attempt - 1) * 15) + (Get-Random -Minimum 0 -Maximum 6) - Write-Host "download failed; sleeping ${delay}s before retry" - Start-Sleep -Seconds $delay - } - } - if (-not $downloaded) { - Write-Error "Failed to download ffmpeg after $attempts attempts" - exit 1 - } - Expand-Archive -Path $env:FFMPEG_ASSET -DestinationPath ffmpeg-extract -Force - New-Item -ItemType Directory -Force -Path C:\ffmpeg-bin | Out-Null - Copy-Item -Path (Join-Path "ffmpeg-extract" "ffmpeg-$env:FFMPEG_VERSION-essentials_build\bin\*.exe") -Destination C:\ffmpeg-bin\ - Remove-Item -Recurse -Force ffmpeg-extract, $env:FFMPEG_ASSET - - - name: Add ffmpeg to PATH (Windows) - if: ${{ steps.check.outputs.installed == 'false' && runner.os == 'Windows' }} - shell: pwsh - run: Add-Content -Path $env:GITHUB_PATH -Value 'C:\ffmpeg-bin' - - - name: Verify ffmpeg - shell: bash - run: ffmpeg -version | head -n 1 diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml deleted file mode 100644 index 495d68c3..00000000 --- a/.github/workflows/build-windows.yml +++ /dev/null @@ -1,134 +0,0 @@ -# Build Portable Windows EXE (x64) Distribution for PySceneDetect - -name: Windows Distribution - -on: - schedule: - - cron: '0 0 * * *' - pull_request: - paths: - - packaging/** - - scripts/** - - scenedetect/** - - tests/** - - pyproject.toml - - .github/workflows/build-windows.yml - push: - paths: - - packaging/** - - scripts/** - - scenedetect/** - - tests/** - - pyproject.toml - - .github/workflows/build-windows.yml - branches: - - main - - 'releases/**' - tags: - - 'v*' - workflow_dispatch: - -jobs: - build: - runs-on: windows-latest - strategy: - matrix: - python-version: ["3.13"] - - env: - ffmpeg-version: "8.1" - - steps: - - uses: actions/checkout@v5 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip build wheel virtualenv setuptools - pip install .[docs] - pip install --upgrade -r packaging/windows/requirements.txt --no-binary imageio-ffmpeg - - - name: Download Resources - run: | - git fetch --depth=1 https://github.com/Breakthrough/PySceneDetect.git refs/heads/resources:refs/remotes/origin/resources - git checkout refs/remotes/origin/resources -- tests/resources/ - - - name: Download FFMPEG ${{ env.ffmpeg-version }} - uses: dsaltares/fetch-gh-release-asset@1.1.2 - with: - repo: 'GyanD/codexffmpeg' - version: 'tags/${{ env.ffmpeg-version }}' - file: 'ffmpeg-${{ env.ffmpeg-version }}-full_build.7z' - - - name: Unit Test - shell: bash - run: | - 7z e ffmpeg-${{ env.ffmpeg-version }}-full_build.7z ffmpeg.exe -r - 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 - run: | - python scripts/pre_release.py - pyinstaller packaging/windows/scenedetect.spec - - - name: Build Documentation - run: | - sphinx-build -b singlehtml docs dist/scenedetect/docs - rm -r dist/scenedetect/docs/.doctrees - - - name: Assemble Portable Distribution - run: | - Move-Item -Path LICENSE -Destination dist/scenedetect/ - New-Item -Path dist/scenedetect/ -Name thirdparty -ItemType Directory - Copy-Item -Path packaging/windows/LICENSE-PYTHON -Destination dist/scenedetect/thirdparty/ - Copy-Item -Path scenedetect/_thirdparty/LICENSE* -Destination dist/scenedetect/thirdparty/ - 7z e -odist/ffmpeg ffmpeg-${{ env.ffmpeg-version }}-full_build.7z LICENSE -r - Move-Item -Path ffmpeg.exe -Destination dist/scenedetect/ffmpeg.exe - Move-Item -Path dist/ffmpeg/LICENSE -Destination dist/scenedetect/thirdparty/LICENSE-FFMPEG - - - name: Test Portable Distribution - run: | - ./dist/scenedetect/scenedetect -i tests/resources/goldeneye.mp4 detect-content time -e 2s - - - name: Upload Artifact - uses: actions/upload-artifact@v6 - with: - name: PySceneDetect-win64 - path: dist/scenedetect - include-hidden-files: true - - test: - runs-on: windows-latest - needs: build - steps: - - uses: actions/checkout@v5 - with: - ref: resources - - - uses: actions/download-artifact@v7 - with: - name: PySceneDetect-win64 - path: build - - - name: Test - run: | - echo Testing binary - ./build/scenedetect version - echo Test OpenCV - ./build/scenedetect -i tests/resources/goldeneye.mp4 -b opencv detect-content time --end 10s - echo Test PyAV - ./build/scenedetect -i tests/resources/goldeneye.mp4 -b pyav detect-content time --end 10s - echo Test moviepy - ./build/scenedetect -i tests/resources/goldeneye.mp4 -b moviepy detect-content time --end 10s - echo Test split-video + ffmpeg - ./build/scenedetect -i tests/resources/goldeneye.mp4 detect-content time --end 10s split-video diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index d6f0f6ef..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,161 +0,0 @@ -# Test PySceneDetect on Linux/OSX/Windows and generate Python distribution (sdist/wheel). -name: Python Distribution - -on: - schedule: - - cron: '0 0 * * *' - pull_request: - paths: - - packaging/** - - scripts/** - - scenedetect/** - - tests/** - - pyproject.toml - - .github/workflows/build.yml - - .github/actions/setup-ffmpeg/** - push: - paths: - - packaging/** - - scripts/** - - scenedetect/** - - tests/** - - pyproject.toml - - .github/workflows/build.yml - - .github/actions/setup-ffmpeg/** - branches: - - main - - 'releases/**' - tags: - - 'v*' - workflow_dispatch: - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-14, macos-latest, ubuntu-22.04, ubuntu-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] - env: - # Version is extracted below and used to find correct package install path. - scenedetect_version: "" - - steps: - - uses: actions/checkout@v5 - - - name: Setup FFmpeg - uses: ./.github/actions/setup-ffmpeg - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip build wheel virtualenv setuptools - pip install -e .[dev] --only-binary av,opencv-python - - - name: Checkout test resources - run: | - git fetch --depth=1 https://github.com/Breakthrough/PySceneDetect.git refs/heads/resources:refs/remotes/origin/resources - git checkout refs/remotes/origin/resources -- tests/resources/ - - # Instrumented while chasing a windows-latest flake where python exits 1 after a - # fully green pytest run with no output. `-X dev` makes shutdown-time warnings and - # finalizer errors loud; echoing the exit code separates python's own return value - # from anything the shell wrapper does to a crash code. - - name: Unit Tests - shell: bash - run: | - set +e - python -X dev -m pytest -vv - code=$? - echo "pytest exit code: $code" - exit $code - - - name: Smoke Test (Module) - run: | - python -m scenedetect version - python -m scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s - python -m scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s - python -m pip uninstall -y scenedetect-core - - - name: Build Package - shell: bash - run: | - # Builds the scenedetect/scenedetect-headless packages. - python packaging/build_all.py - echo "scenedetect_version=`python -c \"import scenedetect; print(scenedetect.__version__.replace('-', '.'))\"`" >> "$GITHUB_ENV" - - - name: Smoke Test Package (Source Dist) - shell: bash - run: | - python -m venv .smoke-sdist - VENV_BIN=.smoke-sdist/bin - [ -d .smoke-sdist/Scripts ] && VENV_BIN=.smoke-sdist/Scripts - source "$VENV_BIN/activate" - pip install "dist/scenedetect-${{ env.scenedetect_version }}.tar.gz[pyav]" --only-binary av - 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}'" - scenedetect version - scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s - scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s - - - name: Smoke Test Package (Wheel) - shell: bash - run: | - python -m venv .smoke-wheel - VENV_BIN=.smoke-wheel/bin - [ -d .smoke-wheel/Scripts ] && VENV_BIN=.smoke-wheel/Scripts - source "$VENV_BIN/activate" - pip install "dist/scenedetect-${{ env.scenedetect_version }}-py3-none-any.whl[pyav]" --only-binary av - scenedetect version - scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s - scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s - - - name: Smoke Test Package (Headless Wheel) - shell: bash - run: | - python -m venv .smoke-headless - VENV_BIN=.smoke-headless/bin - [ -d .smoke-headless/Scripts ] && VENV_BIN=.smoke-headless/Scripts - source "$VENV_BIN/activate" - pip install "dist/scenedetect_headless-${{ env.scenedetect_version }}-py3-none-any.whl[pyav]" --only-binary av - # Confirm the install pulled `opencv-python-headless`, not the GUI variant. - # If both are present, cv2 imports may silently come from either. - 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-headless'], f'Expected only opencv-python-headless, got: {ds}'" - scenedetect version - scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s - scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s - - - name: Smoke Test Package (Upgrade From 0.7) - shell: bash - run: | - # Both packages ship the code (no metapackage layering) precisely so that - # in-place upgrades from older installs keep working - a code-carrying dist - # flipped to a code-free metapackage would break here, since uninstalling the old - # version deletes module files a dependency just wrote. This is also why the - # short-lived scenedetect-core (published in 0.7.1 only) was yanked rather than - # layered on. Keep this as a regression guard for that failure mode - # (https://scenedetect.com/issues/558). - python -m venv .smoke-upgrade - VENV_BIN=.smoke-upgrade/bin - [ -d .smoke-upgrade/Scripts ] && VENV_BIN=.smoke-upgrade/Scripts - source "$VENV_BIN/activate" - pip install "scenedetect==0.7" - pip install --upgrade --find-links dist/ "scenedetect==${{ env.scenedetect_version }}" - python -c "import scenedetect; print(scenedetect.__version__)" - scenedetect version - - - name: Upload Package - if: ${{ matrix.python-version == '3.13' && matrix.os == 'ubuntu-latest' }} - uses: actions/upload-artifact@v6 - with: - name: scenedetect-dist - path: | - dist/*.tar.gz - dist/*.whl diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml deleted file mode 100644 index 199c5ef3..00000000 --- a/.github/workflows/check-docs.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Check Documentation - -on: - schedule: - - cron: '0 0 * * *' - pull_request: - paths: - - docs/** - - scenedetect/** - - website/** - push: - paths: - - docs/** - - scenedetect/** - - website/** - branches: - - main - - 'releases/**' - tags: - - 'v*' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v5 - - - name: Set up Python 3.12 - uses: actions/setup-python@v6 - with: - python-version: '3.12' - cache: 'pip' - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip build wheel virtualenv - pip install .[docs,website] - pip install -r packaging/windows/requirements.txt - - - - name: Check CLI Documentation - shell: bash - run: | - if [[ `git status --porcelain=1 | wc -l` -ne 0 ]]; then - echo "CLI documentation is of date: docs/cli.rst does not match output after running docs/generate_cli_docs.py!" - echo "Re-run `python docs/generate_cli_docs.py` to update and commit the result." - exit 1 - fi - - - name: Build Sphinx Reference (warnings as errors) - shell: bash - run: | - sphinx-build -W --keep-going -b html docs docs/_build/html - - - name: Build MkDocs Website (--strict) - shell: bash - run: | - mkdocs build --strict -f website/mkdocs.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 9dd65e17..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,52 +0,0 @@ -# CodeQL for PySceneDetect -name: "CodeQL" - -on: - push: - branches: - - main - - releases/** - paths: - - scenedetect/** - - tests/** - pull_request: - branches: - - main - - releases/* - paths: - - scenedetect/** - - tests/** - schedule: - - cron: "20 7 * * 4" - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ python ] - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: +security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index 046e9c88..00000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Dependency Review Action -# -# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. -# -# Source repository: https://github.com/actions/dependency-review-action -# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement -name: 'Dependency Review' -on: [pull_request] - -permissions: - contents: read - -jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: 'Checkout Repository' - uses: actions/checkout@v5 - - name: 'Dependency Review' - uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 0ce991b7..00000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,72 +0,0 @@ -# Build and publish PySceneDetect Docker image to GitHub Container Registry (GHCR). -name: Publish Docker Image - -# Publishing a release build is driven by release.yml, which dispatches this workflow after -# artifact verification passes. -on: - workflow_dispatch: - inputs: - tag_latest: - description: 'Also tag this build as `latest`' - type: boolean - default: false - push: - branches: [ "main" ] - -env: - REGISTRY: ghcr.io - # Image names must be lowercase; github.repository is Breakthrough/PySceneDetect. - IMAGE_NAME: breakthrough/pyscenedetect - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && inputs.tag_latest }} - - - name: Build and push Docker image - id: push - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true - diff --git a/.github/workflows/generate-docs.yml b/.github/workflows/generate-docs.yml deleted file mode 100644 index 12ccd7a2..00000000 --- a/.github/workflows/generate-docs.yml +++ /dev/null @@ -1,93 +0,0 @@ -# Generate PySceneDetect documentation and updates the gh-pages branch. -name: Generate Documentation - -on: - push: - branches: - - main # docs/head - - 'releases/**' # docs/** - paths: - - 'docs/**' - workflow_dispatch: - -jobs: - update_docs: - runs-on: ubuntu-latest - permissions: - contents: write # pushes generated docs to the gh-pages branch - env: - scenedetect_docs_dest: '' - - steps: - - uses: actions/checkout@v5 - - - name: Get Latest Version - run: | - git fetch origin main --depth=1 - echo "scenedetect_docs_latest=$(git show origin/main:docs/LATEST_VERSION | tr -d '[:space:]')" >> "$GITHUB_ENV" - - - name: Set up Python 3.12 - uses: actions/setup-python@v6 - with: - python-version: '3.12' - cache: 'pip' - - - name: Set Destination (Releases) - if: ${{ contains(github.ref_name, 'releases') }} - run: | - echo "scenedetect_docs_dest=$(echo ${{ github.ref_name }} | cut -b 10-)" >> "$GITHUB_ENV" - - - name: Set Destination (Head) - if: ${{ contains(github.ref_name, 'main') }} - run: | - echo "scenedetect_docs_dest=head" >> "$GITHUB_ENV" - - - name: Check Destination - if: ${{ env.scenedetect_docs_dest == '' }} - run: | - echo "Failing build: destination must be set!" - - - name: Setup Environment - run: | - python -m pip install --upgrade pip build wheel virtualenv - pip install .[docs] - pip install -r packaging/windows/requirements.txt - git config --global user.name github-actions - git config --global user.email github-actions@github.com - - - name: Check CLI Documentation - shell: bash - run: | - if [[ `git status --porcelain=1 | wc -l` -ne 0 ]]; then - echo "CLI documentation is of date: docs/cli.rst does not match output after running docs/generate_cli_docs.py!" - echo "Re-run `python docs/generate_cli_docs.py` to update and commit the result." - exit 1 - fi - - - name: Generate Docs - run: | - sphinx-build -b html docs build - - - name: Update gh-pages Branch - run: | - git fetch origin gh-pages - git checkout gh-pages - git rm "docs/${{ env.scenedetect_docs_dest }}" -r -f --ignore-unmatch - git add build/ - git mv build "docs/${{ env.scenedetect_docs_dest }}" - - - name: Update Latest - if: ${{ env.scenedetect_docs_dest == env.scenedetect_docs_latest }} - run: | - git rm "docs/latest" -r -f --ignore-unmatch - mkdir -p latest - cp -r -f "docs/${{ env.scenedetect_docs_dest }}" docs/latest - git add docs/latest - echo "scenedetect_docs_dest='${{ env.scenedetect_docs_dest }} (latest)'" >> "$GITHUB_ENV" - - - name: Commit and Push - run: | - git commit -a -m "[docs] @${{ github.triggering_actor }}: Generate Documentation" \ - -m "Source: ${{ github.ref_name }} (${{ github.sha }})" \ - -m "Destination: ${{ env.scenedetect_docs_dest }}" - git push diff --git a/.github/workflows/generate-website.yml b/.github/workflows/generate-website.yml deleted file mode 100644 index 6e924106..00000000 --- a/.github/workflows/generate-website.yml +++ /dev/null @@ -1,53 +0,0 @@ - -name: Generate Website - -on: - push: - branches: - - main - paths: - - 'website/**' - workflow_dispatch: - -jobs: - update_site: - runs-on: ubuntu-latest - permissions: - contents: write # pushes generated site to the gh-pages branch - - steps: - - uses: actions/checkout@v5 - - - name: Set up Python 3.12 - uses: actions/setup-python@v6 - with: - python-version: '3.12' - cache: 'pip' - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip build wheel virtualenv - pip install .[website] - - - name: Generate Website - run: | - mkdocs build -f website/mkdocs.yml - - - name: Update Website - run: | - git fetch origin gh-pages - git checkout gh-pages - git rm * -r -f --ignore-unmatch - git checkout HEAD -- .nojekyll - git checkout HEAD -- CNAME - git checkout HEAD -- docs/ - git rm docs/index.html --ignore-unmatch - git add website/build/ - git mv website/build/* . -f -k - git mv website/build/docs/index.html docs/index.html -k - git rm website/build/* -r -f --ignore-unmatch - git config --global user.name github-actions - git config --global user.email github-actions@github.com - git commit -a -m "[docs] @${{ github.triggering_actor }}: Generate Website" \ - -m "Commit: ${{ github.sha }}" - git push diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml deleted file mode 100644 index de468367..00000000 --- a/.github/workflows/publish-pypi.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Publish PyPI Package - -on: - workflow_dispatch: - inputs: - tag: - description: 'Tag To Publish' - required: true - environment: - description: 'PyPI Environment' - required: true - type: choice - options: - - testpypi - - pypi - default: 'testpypi' - -jobs: - # Production publishes require all three release workflows to be green for the tag. - # TestPyPI publishes are exploratory and skip this job; the Resolve step in `publish` - # still gates on a successful Python Distribution run so artifacts are guaranteed to exist. - verify: - name: Verify Build - if: github.event.inputs.environment == 'pypi' - runs-on: ubuntu-latest - permissions: - actions: read - steps: - - name: Check required workflows succeeded for ${{ github.event.inputs.tag }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - TAG: ${{ github.event.inputs.tag }} - run: | - set -euo pipefail - required=("Python Distribution" "Windows Distribution" "Release Test Suite") - failed=0 - for workflow in "${required[@]}"; do - conclusion=$(gh run list \ - --workflow "$workflow" \ - --branch "$TAG" \ - --event push \ - --limit 1 \ - --json conclusion \ - -q '.[0].conclusion // ""') - if [[ "$conclusion" != "success" ]]; then - echo "::error::Workflow '$workflow' did not succeed for tag $TAG (got: '${conclusion:-no run found}')" - failed=1 - else - echo "[OK] $workflow" - fi - done - [[ "$failed" -eq 0 ]] - - publish: - name: Publish ${{ github.event.inputs.tag }} to ${{ github.event.inputs.environment }} - runs-on: ubuntu-latest - needs: verify - # Run when verify succeeded (pypi) or was skipped (testpypi). - if: | - always() && - (needs.verify.result == 'success' || needs.verify.result == 'skipped') - - environment: - name: ${{ github.event.inputs.environment }} - url: ${{ github.event.inputs.environment == 'testpypi' && 'https://test.pypi.org/p/scenedetect' || 'https://pypi.org/p/scenedetect' }} - - permissions: - id-token: write # mandatory for trusted publishing - actions: read # for cross-workflow artifact download - - steps: - - name: Resolve Python Distribution run for ${{ github.event.inputs.tag }} - id: resolve - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - TAG: ${{ github.event.inputs.tag }} - run: | - set -euo pipefail - run_id=$(gh run list \ - --workflow "Python Distribution" \ - --branch "$TAG" \ - --event push \ - --status success \ - --limit 1 \ - --json databaseId \ - -q '.[0].databaseId // ""') - if [[ -z "$run_id" ]]; then - echo "::error::No successful 'Python Distribution' run found for tag $TAG. Push the tag and wait for build.yml to finish before publishing." - exit 1 - fi - echo "run-id=$run_id" >> "$GITHUB_OUTPUT" - echo "Using Python Distribution run $run_id" - - - name: Download distribution artifact - uses: actions/download-artifact@v7 - with: - name: scenedetect-dist - path: pkg/ - github-token: ${{ secrets.GITHUB_TOKEN }} - repository: ${{ github.repository }} - run-id: ${{ steps.resolve.outputs.run-id }} - - - name: List artifact contents - # Expect 4 files: sdist + wheel for each of scenedetect and - # scenedetect-headless. Both projects publish from this one step; - # each needs a trusted publisher configured on PyPI/TestPyPI for this workflow. - run: ls -la pkg/ - - - name: Publish Package - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: ${{ github.event.inputs.environment == 'testpypi' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }} - packages-dir: pkg/ - print-hash: true - # Tolerate retries: skip existing packages if for example only some variants were uploaded. - skip-existing: true diff --git a/.github/workflows/release-test.yml b/.github/workflows/release-test.yml deleted file mode 100644 index 4c6de70d..00000000 --- a/.github/workflows/release-test.yml +++ /dev/null @@ -1,117 +0,0 @@ -name: Release Test Suite - -on: - workflow_dispatch: - push: - tags: - - 'v*' - -jobs: - static: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.10' - cache: 'pip' - - name: Install dependencies - run: | - # setuptools is upgraded because the toolcache copy periodically lags - # security fixes (e.g. PYSEC-2026-3447) and would fail the audit below. - python -m pip install --upgrade pip setuptools - pip install build twine pip-audit - - name: Version consistency check - run: | - # Parse __version__ directly so we don't have to install scenedetect - # (importing it triggers a cv2-availability guard). - VERSION=$(python -c "import ast,pathlib; print(next(n.value.value for n in ast.parse(pathlib.Path('scenedetect/__init__.py').read_text()).body if isinstance(n, ast.Assign) and any(getattr(t,'id',None)=='__version__' for t in n.targets)))") - echo "scenedetect.__version__ = $VERSION" - if [[ "${{ github.ref }}" == refs/tags/* ]]; then - TAG_VERSION=${GITHUB_REF#refs/tags/v} - TAG_VERSION=${TAG_VERSION%-release} - if [[ "$VERSION" != "$TAG_VERSION" ]]; then - echo "Version mismatch: scenedetect=$VERSION, tag=$TAG_VERSION" - exit 1 - fi - # Pre-release tags (e.g. 0.7-dev0) ship before the changelog is finalized, - # so only enforce the heading on stable releases. - if [[ "$TAG_VERSION" == *-dev* ]]; then - echo "Pre-release ($TAG_VERSION); skipping changelog heading check." - # Major/minor releases use a '## PySceneDetect X.Y' heading; patch - # releases nest under it as '### PySceneDetect X.Y.Z (date)'. - elif ! grep -Eq "^#{2,3} (PySceneDetect )?$TAG_VERSION( |$)" website/pages/changelog.md; then - echo "Changelog is missing a heading for $TAG_VERSION (e.g. '### PySceneDetect $TAG_VERSION (...)')" - exit 1 - fi - fi - - name: Build and Check - run: | - # Builds the scenedetect/scenedetect-headless packages. - python packaging/build_all.py - # Glob by extension: dist/ also holds tracked website assets (dist/logo/), - # which `twine check dist/*` would reject as an unknown distribution. - twine check dist/*.whl dist/*.tar.gz - - name: pip-audit - # CVE-2026-3219 in pip 26.0.1 has no fix version available upstream - # and pip ships pre-installed on the runner (not controlled by this - # project). Re-evaluate when pip publishes a fix. - run: pip-audit --ignore-vuln CVE-2026-3219 - - release-tests: - needs: static - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.10', '3.13'] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v5 - - name: Checkout resources branch - run: | - git fetch --depth=1 origin refs/heads/resources:refs/remotes/origin/resources - git checkout refs/remotes/origin/resources -- tests/resources/ - git reset - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - name: Install ffmpeg - uses: ./.github/actions/setup-ffmpeg - - name: Install dependencies - run: | - python -m pip install --upgrade pip - # The dev extra supplies the CLI deps (click/opencv/tqdm) plus av and moviepy; - # a bare `pip install .` is now scenedetect-core with numpy only. - pip install .[dev] - pip install opentimelineio pillow psutil pytest - - name: Run release tests - run: pytest -m release -vv --ignore=tests/release/test_long_video_stress.py - - long-stress: - needs: static - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Checkout resources branch - run: | - git fetch --depth=1 origin refs/heads/resources:refs/remotes/origin/resources - git checkout refs/remotes/origin/resources -- tests/resources/ - git reset - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.10' - cache: 'pip' - - name: Install ffmpeg - uses: ./.github/actions/setup-ffmpeg - - name: Install dependencies - run: | - python -m pip install --upgrade pip - # The dev extra supplies the CLI deps (click/opencv/tqdm) plus av. - pip install .[dev] - pip install psutil pytest - - name: Run long stress test - run: pytest -m release -k long_video -vv diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 80f5a530..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,217 +0,0 @@ -# Release orchestrator: verifies the artifacts attached to a DRAFT GitHub -# release actually work, then publishes stage by stage, verifying each stage -# before starting the next: -# -# MSI install/upgrade test against the draft's assets (test-installer.yml) -# -> TestPyPI publish -> pip smoke install from TestPyPI -# -> publish the GitHub release (draft -> public, marked latest) -# -> PyPI publish -> pip smoke install from PyPI -# -> Docker publish -> docker pull + smoke run from GHCR -# -# This workflow is dispatched manually once the GitHub release has been DRAFTED -# with its artifacts attached (signed MSI/zip, wheels, SHA256SUMS); nothing goes -# public until artifact verification passes. Each stage is driven through the -# existing workflows via `gh workflow run` (rather than workflow_call) so they -# keep working standalone and the PyPI trusted-publisher configuration (which is -# bound to publish-pypi.yml as the top-level workflow) is unaffected. - -name: Release Orchestrator - -on: - workflow_dispatch: - inputs: - tag: - description: 'Release tag to verify and publish (e.g. v0.7.1)' - required: true - verify-only: - description: 'Stop after verification (no PyPI/Docker publish)' - type: boolean - default: false - -permissions: - contents: write # read the draft release's assets and publish it (draft -> public) - actions: write # `gh workflow run` on the workflows this one orchestrates - -# The run-id lookup after each dispatch assumes this is the only orchestrator -# running; never allow two concurrent releases. -concurrency: - group: release-orchestrator - -jobs: - orchestrate: - name: ${{ inputs.verify-only && 'Verify' || 'Verify + Publish' }} ${{ inputs.tag }} - runs-on: ubuntu-latest - timeout-minutes: 120 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - TAG: ${{ inputs.tag }} - steps: - - name: Validate release - run: | - set -euo pipefail - state=$(gh release view "$TAG" --json isDraft,isPrerelease \ - -q 'if .isDraft then "draft" elif .isPrerelease then "prerelease" else "published" end') - if [[ "$state" == "draft" ]]; then - echo "Release $TAG is a draft; it will be published after artifact verification passes." - else - echo "Release $TAG is already $state; the GitHub release publish step will be a no-op (re-run mode)." - fi - # Display version used by the pip smoke test; mirrors the tag - # normalization in release-test.yml (both vX.Y[.Z] and the legacy - # vX.Y[.Z]-release tag styles are accepted). - VERSION="${TAG#v}" - VERSION="${VERSION%-release}" - echo "VERSION=$VERSION" >> "$GITHUB_ENV" - - - name: Write dispatch helper - # `gh workflow run` returns no run id, so the helper polls for the - # newest workflow_dispatch run created at/after dispatch time (the - # concurrency group above guarantees it is ours), then watches it to - # completion, propagating failure. - run: | - cat > "$RUNNER_TEMP/dispatch.sh" <<'EOF' - dispatch_and_watch() { - local workflow="$1" ref="$2" - shift 2 - local start_epoch - start_epoch=$(date -u +%s) - echo "::group::Dispatch $workflow (ref $ref) $*" - gh workflow run "$workflow" --ref "$ref" "$@" - local run_id="" - for _ in $(seq 1 24); do - sleep 5 - run_id=$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 5 \ - --json databaseId,createdAt \ - -q "[.[] | select((.createdAt | fromdateiso8601) >= $((start_epoch - 5)))] | first | .databaseId // \"\"") - [[ -n "$run_id" ]] && break - done - if [[ -z "$run_id" ]]; then - echo "::error::Dispatched $workflow but its run never appeared." - return 1 - fi - echo "Watching run $run_id: https://github.com/$GH_REPO/actions/runs/$run_id" - echo "::endgroup::" - gh run watch "$run_id" --exit-status --interval 30 - } - EOF - - - name: Write pip smoke-install helper - # Same smoke test for TestPyPI and production PyPI: fresh venv, install - # the exact release version, and check `scenedetect version` reports it. - # Both indexes can lag a fresh upload, so installs are retried briefly. - run: | - cat > "$RUNNER_TEMP/smoke.sh" <<'EOF' - pip_smoke_install() { - local venv="$1" - shift - python3 -m venv "$venv" - local ok=0 - for attempt in 1 2 3 4 5; do - if "$venv/bin/pip" install --quiet "$@" "scenedetect==$VERSION"; then - ok=1 - break - fi - echo "pip install attempt $attempt failed; retrying in 30s..." - sleep 30 - done - [[ "$ok" -eq 1 ]] - local out - out=$("$venv/bin/scenedetect" version) - echo "$out" - grep -F "$VERSION" <<<"$out" - } - EOF - - - name: 'Stage 1 - Verify: Windows installer (install + upgrade on clean runner)' - run: | - set -euo pipefail - source "$RUNNER_TEMP/dispatch.sh" - dispatch_and_watch test-installer.yml "$GITHUB_REF_NAME" -f "tag=$TAG" - - - name: 'Stage 2 - Publish: TestPyPI' - run: | - set -euo pipefail - source "$RUNNER_TEMP/dispatch.sh" - # Dispatched on the release tag: the pypi environment's deployment - # branch policy only permits release refs (v* tags / releases/* - # branches), and the tag is the immutable ref being released anyway. - dispatch_and_watch publish-pypi.yml "$TAG" -f "tag=$TAG" -f "environment=testpypi" - - - name: 'Stage 2 - Verify: pip install from TestPyPI' - run: | - set -euo pipefail - source "$RUNNER_TEMP/smoke.sh" - # Dependencies are not mirrored on TestPyPI, so resolve them from the - # production index. - pip_smoke_install smoke-testpypi \ - --index-url https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ - - - name: 'Stage 3 - Publish: GitHub release (draft -> public)' - if: ${{ !inputs.verify-only }} - run: | - set -euo pipefail - is_draft=$(gh release view "$TAG" --json isDraft -q .isDraft) - if [[ "$is_draft" == "true" ]]; then - gh release edit "$TAG" --draft=false --latest - echo "Published release $TAG (marked as latest)." - else - echo "Release $TAG is already published; skipping." - fi - - - name: 'Stage 4 - Publish: PyPI (production)' - if: ${{ !inputs.verify-only }} - run: | - set -euo pipefail - source "$RUNNER_TEMP/dispatch.sh" - # publish-pypi.yml additionally gates production publishes on the - # build + release-test workflows being green for the tag. Dispatched - # on the tag ref to satisfy the pypi environment's deployment policy. - dispatch_and_watch publish-pypi.yml "$TAG" -f "tag=$TAG" -f "environment=pypi" - - - name: 'Stage 4 - Verify: pip install from PyPI' - if: ${{ !inputs.verify-only }} - run: | - set -euo pipefail - source "$RUNNER_TEMP/smoke.sh" - pip_smoke_install smoke-pypi - - - name: 'Stage 5 - Publish: Docker image (version tags + latest)' - if: ${{ !inputs.verify-only }} - run: | - set -euo pipefail - source "$RUNNER_TEMP/dispatch.sh" - # Dispatched on the release tag itself so docker/metadata-action - # derives the semver image tags from it (requires the tag to contain - # docker-publish.yml, i.e. v0.7.1 or newer). - dispatch_and_watch docker-publish.yml "$TAG" -f "tag_latest=true" - - - name: 'Stage 5 - Verify: docker pull + smoke run from GHCR' - if: ${{ !inputs.verify-only }} - run: | - set -euo pipefail - image="ghcr.io/breakthrough/pyscenedetect" - docker pull "$image:$VERSION" - docker pull "$image:latest" - # `latest` must point at the build we just published. - v=$(docker image inspect "$image:$VERSION" --format '{{.Id}}') - l=$(docker image inspect "$image:latest" --format '{{.Id}}') - if [[ "$v" != "$l" ]]; then - echo "::error::latest ($l) does not match $VERSION ($v)" - exit 1 - fi - out=$(docker run --rm "$image:$VERSION" version) - echo "$out" - grep -F "$VERSION" <<<"$out" - - - name: Summary - run: | - if [[ "${{ inputs.verify-only }}" == "true" ]]; then - echo "Verification of $TAG passed (release left as draft). Re-run without verify-only to publish." - else - echo "Release $TAG verified and published:" - echo " https://pypi.org/project/scenedetect/$VERSION/" - echo " https://pypi.org/project/scenedetect-headless/$VERSION/" - echo " https://github.com/$GH_REPO/pkgs/container/pyscenedetect" - fi diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml deleted file mode 100644 index f2d432f3..00000000 --- a/.github/workflows/static-analysis.yml +++ /dev/null @@ -1,47 +0,0 @@ -# Check PySceneDetect code lint warnings and formatting. -name: Static Analysis - -on: - pull_request: - paths: - - scenedetect/** - - tests/** - - pyproject.toml - - .github/workflows/static-analysis.yml - push: - paths: - - scenedetect/** - - tests/** - - pyproject.toml - - .github/workflows/static-analysis.yml - -jobs: - check_format: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v5 - - name: Set up Python 3.12 - uses: actions/setup-python@v6 - with: - python-version: '3.12' - cache: 'pip' - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e .[dev] --only-binary av,opencv-python - - - name: Static Analysis (ruff) - run: | - python -m pip install --upgrade ruff - python -m ruff check - python -m ruff format --check - - - name: Type Check (pyright) - env: - PYRIGHT_PYTHON_FORCE_VERSION: latest - run: | - python -m pip install pyright - python -m pyright - diff --git a/.github/workflows/test-installer.yml b/.github/workflows/test-installer.yml deleted file mode 100644 index 45f77a5e..00000000 --- a/.github/workflows/test-installer.yml +++ /dev/null @@ -1,657 +0,0 @@ -# Post-release verification of the signed Windows MSI installer. -# -# Downloads the MSI attached to a published release, then on a clean -# windows-latest runner: verifies checksum + Authenticode signature, performs a -# silent install, checks the Apps & Features registration / PATH / smoke-runs -# the CLI, and uninstalls cleanly. A second job installs the previous release's -# MSI first and upgrades over it to catch duplicate-entry and leftover-file -# regressions. - -name: Windows Installer Test - -# Dispatched by release.yml (the release orchestrator) as part of post-release -# artifact verification; can also be run manually against any published release. -on: - workflow_dispatch: - inputs: - tag: - description: 'Release tag to test (e.g. v0.7-release or v0.7.1)' - required: true - previous-tag: - description: 'Release tag to upgrade from (default: auto-detect previous release)' - required: false - -# NOTE: This workflow never writes to the repository; `contents: write` is required -# only because draft-release assets are invisible to read-only tokens, and the -# release orchestrator runs this verification while the release is still a draft. -permissions: - contents: write - -jobs: - resolve: - name: Resolve Release Tags - runs-on: ubuntu-latest - outputs: - tag: ${{ steps.resolve.outputs.tag }} - version: ${{ steps.resolve.outputs.version }} - msi-version: ${{ steps.resolve.outputs.msi-version }} - prev-tag: ${{ steps.resolve.outputs.prev-tag }} - prev-msi-version: ${{ steps.resolve.outputs.prev-msi-version }} - steps: - - name: Resolve current and previous release - id: resolve - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - INPUT_TAG: ${{ inputs.tag }} - INPUT_PREV_TAG: ${{ inputs.previous-tag }} - run: | - set -euo pipefail - TAG="$INPUT_TAG" - if [[ -z "$TAG" ]]; then - echo "::error::No tag to test: the 'tag' input is required." - exit 1 - fi - # Both tag styles are in use (v0.7-release and v0.7.1); mirror the - # normalization release-test.yml applies to pushed tags. - VERSION="${TAG#v}" - VERSION="${VERSION%-release}" - # Pad to the numeric X.Y.Z the installer stamps into the MSI - # ProductVersion / DisplayVersion; mirrors msi_version() in - # scripts/_release_common.py ("0.7" -> "0.7.0"). - IFS=. read -r a b c _ <<<"$VERSION" - MSI_VERSION="${a}.${b:-0}.${c:-0}" - - if [[ -n "$INPUT_PREV_TAG" ]]; then - PREV_TAG="$INPUT_PREV_TAG" - else - # Newest-first list of published stable releases; the previous - # release is the entry right after the current tag. - mapfile -t tags < <(gh release list --exclude-drafts --exclude-pre-releases \ - --limit 30 --json tagName -q '.[].tagName') - PREV_TAG="" - found=0 - for i in "${!tags[@]}"; do - if [[ "${tags[$i]}" == "$TAG" ]]; then - found=1 - PREV_TAG="${tags[$((i + 1))]:-}" - break - fi - done - if [[ "$found" -eq 0 ]]; then - # Manual dispatch before the release is published: the current - # tag is not listed yet, so upgrade from the newest listed tag - # that differs from it. - for t in "${tags[@]}"; do - if [[ "$t" != "$TAG" ]]; then - PREV_TAG="$t" - break - fi - done - fi - fi - if [[ -z "$PREV_TAG" ]]; then - echo "::error::Could not determine a previous release to upgrade from; pass the 'previous-tag' input." - exit 1 - fi - PREV_VERSION="${PREV_TAG#v}" - PREV_VERSION="${PREV_VERSION%-release}" - IFS=. read -r a b c _ <<<"$PREV_VERSION" - PREV_MSI_VERSION="${a}.${b:-0}.${c:-0}" - - { - echo "tag=$TAG" - echo "version=$VERSION" - echo "msi-version=$MSI_VERSION" - echo "prev-tag=$PREV_TAG" - echo "prev-msi-version=$PREV_MSI_VERSION" - } >> "$GITHUB_OUTPUT" - echo "Testing $TAG (display $VERSION, MSI $MSI_VERSION); upgrading from $PREV_TAG (MSI $PREV_MSI_VERSION)" - - fresh-install: - name: Fresh Install - runs-on: windows-latest - needs: resolve - env: - TAG: ${{ needs.resolve.outputs.tag }} - VERSION: ${{ needs.resolve.outputs.version }} - MSI_VERSION: ${{ needs.resolve.outputs.msi-version }} - steps: - # Test videos live on the resources branch; this is the same layout - # build-windows.yml smoke-tests the portable distribution against. - - uses: actions/checkout@v5 - with: - ref: resources - - - name: Write helper functions - # Each `run:` step is a fresh pwsh process, so shared functions go into - # a file (in RUNNER_TEMP, which checkout cannot clean away) that later - # steps dot-source. - run: | - $helpers = @' - # All hives an uninstall entry could land in. The MSI installs - # per-machine (the .aip sets ALLUSERS=2 and the runner is elevated), - # so 64-bit HKLM is the expected home; the others are scanned so a - # misplaced entry fails the assertions loudly instead of hiding. - $UninstallRoots = @( - 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' - 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' - 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' - ) - - function Get-PySceneDetectEntries { - # Every uninstall entry (any hive, visible or hidden) for - # PySceneDetect. A healthy install has exactly two: the MSI - # ProductCode key (hidden from Apps & Features by - # ARPSYSTEMCOMPONENT=1, set in PySceneDetect.aip) and the visible - # custom key "PySceneDetect " that carries - # DisplayVersion / InstallLocation. - foreach ($root in $UninstallRoots) { - if (-not (Test-Path $root)) { continue } - foreach ($key in Get-ChildItem $root) { - $props = Get-ItemProperty $key.PSPath -ErrorAction SilentlyContinue - if ($props.DisplayName -like 'PySceneDetect*') { - [pscustomobject]@{ - KeyPath = $key.PSPath - KeyName = $key.PSChildName - DisplayName = $props.DisplayName - DisplayVersion = $props.DisplayVersion - InstallLocation = $props.InstallLocation - SystemComponent = $props.SystemComponent - } - } - } - } - } - - function Get-VisiblePySceneDetectEntries { - # The set Apps & Features actually shows: SystemComponent != 1. - Get-PySceneDetectEntries | Where-Object { $_.SystemComponent -ne 1 } - } - - function Get-UninstallKeyByName { - # The visible key's name embeds the MSI version ("PySceneDetect - # 0.7.0"), so a lookup by name across hives is a precise - # per-version existence check. - param([Parameter(Mandatory)][string]$KeyName) - foreach ($root in $UninstallRoots) { - $path = Join-Path $root $KeyName - if (Test-Path $path) { $path } - } - } - - function Invoke-Msiexec { - # msiexec detaches from the console immediately, so a bare - # `msiexec ...` would return before the Windows Installer service - # finishes (and without the real exit code); Start-Process - # -Wait -PassThru blocks and surfaces it. - param( - [Parameter(Mandatory)][string[]]$MsiArgs, - [Parameter(Mandatory)][string]$LogPath - ) - $log = Join-Path (Get-Location) $LogPath - for ($attempt = 1; $attempt -le 3; $attempt++) { - $p = Start-Process msiexec.exe -ArgumentList ($MsiArgs + @('/L*v', $log)) -Wait -PassThru - switch ($p.ExitCode) { - 0 { Write-Host "msiexec $($MsiArgs -join ' ') succeeded (exit 0)"; return } - 3010 { Write-Host 'msiexec exit 3010 (success, reboot required) - treated as success'; return } - 1618 { - # ERROR_INSTALL_ALREADY_RUNNING: runner provisioning - # sometimes still holds the machine-wide MSI mutex. - Write-Host "msiexec exit 1618 (another install in progress), attempt $attempt of 3" - if ($attempt -lt 3) { Start-Sleep -Seconds 30 } - } - default { throw "msiexec $($MsiArgs -join ' ') failed with exit code $($p.ExitCode); see $LogPath" } - } - } - throw 'msiexec still blocked by another installation (exit 1618) after 3 attempts' - } - - function Get-PathRegistryValues { - # The installer edits PATH in the registry only; neither this - # process nor its children see the change, so assertions must - # read the raw values. Machine PATH is where a per-machine - # install writes (the .aip Environment row uses the '*' system - # prefix); HKCU is read too for completeness. - $values = @() - $machine = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name Path -ErrorAction SilentlyContinue - if ($machine) { $values += $machine.Path } - $user = Get-ItemProperty 'HKCU:\Environment' -Name Path -ErrorAction SilentlyContinue - if ($user) { $values += $user.Path } - $values - } - - function Test-DirOnRegistryPath { - param([Parameter(Mandatory)][string]$Directory) - $needle = $Directory.TrimEnd('\') - foreach ($value in Get-PathRegistryValues) { - # -contains is case-insensitive, matching how Windows treats paths. - if (@($value -split ';' | ForEach-Object { $_.TrimEnd('\') }) -contains $needle) { - return $true - } - } - return $false - } - - function Assert-CleanRemoval { - param([string]$InstallDir) - $entries = @(Get-PySceneDetectEntries) - if ($entries.Count -ne 0) { - $entries | Format-List | Out-String | Write-Host - throw "Expected zero uninstall entries after uninstall, found $($entries.Count)" - } - # Also match stale keys by name in case a leftover key lost its - # DisplayName value. - foreach ($root in $UninstallRoots) { - if (-not (Test-Path $root)) { continue } - $stale = @(Get-ChildItem $root | Where-Object { $_.PSChildName -like 'PySceneDetect*' }) - if ($stale.Count -ne 0) { - throw "Stale uninstall keys remain under ${root}: $($stale.PSChildName -join ', ')" - } - } - if ($InstallDir) { - if (Test-Path (Join-Path $InstallDir 'scenedetect.exe')) { - throw "scenedetect.exe still present in $InstallDir after uninstall" - } - if (Test-DirOnRegistryPath -Directory $InstallDir) { - throw "$InstallDir still present in a PATH registry value after uninstall" - } - } - Write-Host 'Verified clean removal.' - } - '@ - $dest = Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1' - Set-Content -LiteralPath $dest -Value $helpers - Write-Host "Wrote $dest" - - - name: Download release assets - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - gh release download $env:TAG --pattern 'PySceneDetect-*-win64.msi' --pattern 'SHA256SUMS' --dir dist - if ($LASTEXITCODE -ne 0) { throw "gh release download failed for $env:TAG" } - $expected = "PySceneDetect-$env:VERSION-win64.msi" - $msis = @(Get-ChildItem dist -Filter '*.msi') - if ($msis.Count -ne 1 -or $msis[0].Name -ne $expected) { - throw "Expected exactly one MSI named $expected, got: $($msis.Name -join ', ')" - } - if (-not (Test-Path dist\SHA256SUMS)) { throw 'SHA256SUMS missing from release assets' } - - - name: Verify checksum and Authenticode signature - run: | - $msi = "dist\PySceneDetect-$env:VERSION-win64.msi" - $name = Split-Path $msi -Leaf - # SHA256SUMS lines are ` ` (two spaces; the sha256sum -c - # format written by scripts/finalize_windows_dist.py). - $sums = @{} - foreach ($line in Get-Content dist\SHA256SUMS) { - $hash, $entry = $line -split ' ', 2 - $sums[$entry.Trim()] = $hash.Trim() - } - if (-not $sums.ContainsKey($name)) { throw "SHA256SUMS has no entry for $name" } - $actual = (Get-FileHash $msi -Algorithm SHA256).Hash.ToLowerInvariant() - if ($actual -ne $sums[$name].ToLowerInvariant()) { - throw "SHA256 mismatch for ${name}: expected $($sums[$name]), got $actual" - } - Write-Host "Checksum OK: $actual" - $sig = Get-AuthenticodeSignature $msi - if ($sig.Status -ne 'Valid') { - throw "Authenticode signature status is '$($sig.Status)' (expected 'Valid')" - } - Write-Host "Signature OK: $($sig.SignerCertificate.Subject)" - - - name: Install MSI - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - $msi = (Resolve-Path "dist\PySceneDetect-$env:VERSION-win64.msi").Path - Invoke-Msiexec -MsiArgs @('/i', $msi, '/qn', '/norestart') -LogPath install.log - - - name: Verify installation - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - # Locate the install via the registry - never hard-code the path: the - # .aip uses ALLUSERS=2 with a MixedAllUsersInstallLocation custom - # action, so APPDIR is resolved at install time. - $visible = @(Get-VisiblePySceneDetectEntries) - if ($visible.Count -ne 1) { - Get-PySceneDetectEntries | Format-List | Out-String | Write-Host - throw "Expected exactly 1 visible uninstall entry, found $($visible.Count)" - } - $entry = $visible[0] - if ($entry.DisplayVersion -ne $env:MSI_VERSION) { - throw "DisplayVersion is '$($entry.DisplayVersion)', expected '$env:MSI_VERSION'" - } - if (-not $entry.InstallLocation) { throw 'InstallLocation is empty' } - $exe = Join-Path $entry.InstallLocation 'scenedetect.exe' - if (-not (Test-Path $exe)) { throw "scenedetect.exe not found in $($entry.InstallLocation)" } - # Persist the discovered install dir for the later steps. - Add-Content $env:GITHUB_ENV "INSTALL_DIR=$($entry.InstallLocation)" - # Invoke by absolute path: the installer's PATH change is - # registry-only and not visible to this already-running process. - $out = & $exe version 2>&1 | Out-String - Write-Host $out - if ($LASTEXITCODE -ne 0) { throw "scenedetect version exited with $LASTEXITCODE" } - if (-not $out.Contains($env:VERSION)) { throw "version output does not mention $env:VERSION" } - if (-not (Test-DirOnRegistryPath -Directory $entry.InstallLocation)) { - throw "$($entry.InstallLocation) was not added to any PATH registry value" - } - Write-Host 'Install verified.' - - - name: Functional smoke test - run: | - # Same clip and command build-windows.yml uses to smoke-test the - # portable distribution. - $exe = Join-Path $env:INSTALL_DIR 'scenedetect.exe' - & $exe -i tests/resources/goldeneye.mp4 detect-content time --end 10s - if ($LASTEXITCODE -ne 0) { throw "smoke test exited with $LASTEXITCODE" } - - - name: Uninstall MSI - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - # Uninstall via the same local MSI file (not the ProductCode) so a - # broken product registration surfaces as a failure here. - $msi = (Resolve-Path "dist\PySceneDetect-$env:VERSION-win64.msi").Path - Invoke-Msiexec -MsiArgs @('/x', $msi, '/qn', '/norestart') -LogPath uninstall.log - - - name: Verify clean removal - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - Assert-CleanRemoval -InstallDir $env:INSTALL_DIR - - - name: Upload msiexec logs - if: failure() - uses: actions/upload-artifact@v6 - with: - name: fresh-install-logs - path: '*.log' - if-no-files-found: ignore - - upgrade: - name: Upgrade From Previous Release - runs-on: windows-latest - needs: resolve - env: - TAG: ${{ needs.resolve.outputs.tag }} - VERSION: ${{ needs.resolve.outputs.version }} - MSI_VERSION: ${{ needs.resolve.outputs.msi-version }} - PREV_TAG: ${{ needs.resolve.outputs.prev-tag }} - PREV_MSI_VERSION: ${{ needs.resolve.outputs.prev-msi-version }} - steps: - - name: Write helper functions - # Identical to the fresh-install helpers; jobs cannot share script - # blocks, so the definitions are duplicated per job. - run: | - $helpers = @' - # All hives an uninstall entry could land in. The MSI installs - # per-machine (the .aip sets ALLUSERS=2 and the runner is elevated), - # so 64-bit HKLM is the expected home; the others are scanned so a - # misplaced entry fails the assertions loudly instead of hiding. - $UninstallRoots = @( - 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' - 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' - 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' - ) - - function Get-PySceneDetectEntries { - # Every uninstall entry (any hive, visible or hidden) for - # PySceneDetect. A healthy install has exactly two: the MSI - # ProductCode key (hidden from Apps & Features by - # ARPSYSTEMCOMPONENT=1, set in PySceneDetect.aip) and the visible - # custom key "PySceneDetect " that carries - # DisplayVersion / InstallLocation. - foreach ($root in $UninstallRoots) { - if (-not (Test-Path $root)) { continue } - foreach ($key in Get-ChildItem $root) { - $props = Get-ItemProperty $key.PSPath -ErrorAction SilentlyContinue - if ($props.DisplayName -like 'PySceneDetect*') { - [pscustomobject]@{ - KeyPath = $key.PSPath - KeyName = $key.PSChildName - DisplayName = $props.DisplayName - DisplayVersion = $props.DisplayVersion - InstallLocation = $props.InstallLocation - SystemComponent = $props.SystemComponent - } - } - } - } - } - - function Get-VisiblePySceneDetectEntries { - # The set Apps & Features actually shows: SystemComponent != 1. - Get-PySceneDetectEntries | Where-Object { $_.SystemComponent -ne 1 } - } - - function Get-UninstallKeyByName { - # The visible key's name embeds the MSI version ("PySceneDetect - # 0.7.0"), so a lookup by name across hives is a precise - # per-version existence check. - param([Parameter(Mandatory)][string]$KeyName) - foreach ($root in $UninstallRoots) { - $path = Join-Path $root $KeyName - if (Test-Path $path) { $path } - } - } - - function Invoke-Msiexec { - # msiexec detaches from the console immediately, so a bare - # `msiexec ...` would return before the Windows Installer service - # finishes (and without the real exit code); Start-Process - # -Wait -PassThru blocks and surfaces it. - param( - [Parameter(Mandatory)][string[]]$MsiArgs, - [Parameter(Mandatory)][string]$LogPath - ) - $log = Join-Path (Get-Location) $LogPath - for ($attempt = 1; $attempt -le 3; $attempt++) { - $p = Start-Process msiexec.exe -ArgumentList ($MsiArgs + @('/L*v', $log)) -Wait -PassThru - switch ($p.ExitCode) { - 0 { Write-Host "msiexec $($MsiArgs -join ' ') succeeded (exit 0)"; return } - 3010 { Write-Host 'msiexec exit 3010 (success, reboot required) - treated as success'; return } - 1618 { - # ERROR_INSTALL_ALREADY_RUNNING: runner provisioning - # sometimes still holds the machine-wide MSI mutex. - Write-Host "msiexec exit 1618 (another install in progress), attempt $attempt of 3" - if ($attempt -lt 3) { Start-Sleep -Seconds 30 } - } - default { throw "msiexec $($MsiArgs -join ' ') failed with exit code $($p.ExitCode); see $LogPath" } - } - } - throw 'msiexec still blocked by another installation (exit 1618) after 3 attempts' - } - - function Get-PathRegistryValues { - # The installer edits PATH in the registry only; neither this - # process nor its children see the change, so assertions must - # read the raw values. Machine PATH is where a per-machine - # install writes (the .aip Environment row uses the '*' system - # prefix); HKCU is read too for completeness. - $values = @() - $machine = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name Path -ErrorAction SilentlyContinue - if ($machine) { $values += $machine.Path } - $user = Get-ItemProperty 'HKCU:\Environment' -Name Path -ErrorAction SilentlyContinue - if ($user) { $values += $user.Path } - $values - } - - function Test-DirOnRegistryPath { - param([Parameter(Mandatory)][string]$Directory) - $needle = $Directory.TrimEnd('\') - foreach ($value in Get-PathRegistryValues) { - # -contains is case-insensitive, matching how Windows treats paths. - if (@($value -split ';' | ForEach-Object { $_.TrimEnd('\') }) -contains $needle) { - return $true - } - } - return $false - } - - function Assert-CleanRemoval { - param([string]$InstallDir) - $entries = @(Get-PySceneDetectEntries) - if ($entries.Count -ne 0) { - $entries | Format-List | Out-String | Write-Host - throw "Expected zero uninstall entries after uninstall, found $($entries.Count)" - } - # Also match stale keys by name in case a leftover key lost its - # DisplayName value. - foreach ($root in $UninstallRoots) { - if (-not (Test-Path $root)) { continue } - $stale = @(Get-ChildItem $root | Where-Object { $_.PSChildName -like 'PySceneDetect*' }) - if ($stale.Count -ne 0) { - throw "Stale uninstall keys remain under ${root}: $($stale.PSChildName -join ', ')" - } - } - if ($InstallDir) { - if (Test-Path (Join-Path $InstallDir 'scenedetect.exe')) { - throw "scenedetect.exe still present in $InstallDir after uninstall" - } - if (Test-DirOnRegistryPath -Directory $InstallDir) { - throw "$InstallDir still present in a PATH registry value after uninstall" - } - } - Write-Host 'Verified clean removal.' - } - '@ - $dest = Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1' - Set-Content -LiteralPath $dest -Value $helpers - Write-Host "Wrote $dest" - - - name: Download previous release MSI - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - gh release download $env:PREV_TAG --pattern '*-win64.msi' --dir prev - if ($LASTEXITCODE -ne 0) { throw "gh release download failed for $env:PREV_TAG" } - $msis = @(Get-ChildItem prev -Filter '*.msi') - if ($msis.Count -ne 1) { - throw "Expected exactly one MSI from $env:PREV_TAG, got: $($msis.Name -join ', ')" - } - # Older releases may not ship SHA256SUMS, so the previous MSI is - # deliberately not checksummed; only the new MSI under test is. - Add-Content $env:GITHUB_ENV "PREV_MSI=$($msis[0].FullName)" - - - name: Install previous release - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - Invoke-Msiexec -MsiArgs @('/i', $env:PREV_MSI, '/qn', '/norestart') -LogPath install-prev.log - # Sanity check the baseline before upgrading over it. - $visible = @(Get-VisiblePySceneDetectEntries) - if ($visible.Count -ne 1) { - Get-PySceneDetectEntries | Format-List | Out-String | Write-Host - throw "Expected exactly 1 visible uninstall entry after baseline install, found $($visible.Count)" - } - if ($visible[0].DisplayVersion -ne $env:PREV_MSI_VERSION) { - throw "Baseline DisplayVersion is '$($visible[0].DisplayVersion)', expected '$env:PREV_MSI_VERSION'" - } - $exe = Join-Path $visible[0].InstallLocation 'scenedetect.exe' - if (-not (Test-Path $exe)) { throw "scenedetect.exe not found in $($visible[0].InstallLocation)" } - & $exe version - if ($LASTEXITCODE -ne 0) { throw "baseline scenedetect version exited with $LASTEXITCODE" } - Add-Content $env:GITHUB_ENV "OLD_INSTALL_DIR=$($visible[0].InstallLocation)" - - - name: Download new release assets - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - gh release download $env:TAG --pattern 'PySceneDetect-*-win64.msi' --pattern 'SHA256SUMS' --dir dist - if ($LASTEXITCODE -ne 0) { throw "gh release download failed for $env:TAG" } - $expected = "PySceneDetect-$env:VERSION-win64.msi" - $msis = @(Get-ChildItem dist -Filter '*.msi') - if ($msis.Count -ne 1 -or $msis[0].Name -ne $expected) { - throw "Expected exactly one MSI named $expected, got: $($msis.Name -join ', ')" - } - if (-not (Test-Path dist\SHA256SUMS)) { throw 'SHA256SUMS missing from release assets' } - - - name: Verify checksum and Authenticode signature - run: | - $msi = "dist\PySceneDetect-$env:VERSION-win64.msi" - $name = Split-Path $msi -Leaf - # SHA256SUMS lines are ` ` (two spaces; the sha256sum -c - # format written by scripts/finalize_windows_dist.py). - $sums = @{} - foreach ($line in Get-Content dist\SHA256SUMS) { - $hash, $entry = $line -split ' ', 2 - $sums[$entry.Trim()] = $hash.Trim() - } - if (-not $sums.ContainsKey($name)) { throw "SHA256SUMS has no entry for $name" } - $actual = (Get-FileHash $msi -Algorithm SHA256).Hash.ToLowerInvariant() - if ($actual -ne $sums[$name].ToLowerInvariant()) { - throw "SHA256 mismatch for ${name}: expected $($sums[$name]), got $actual" - } - Write-Host "Checksum OK: $actual" - $sig = Get-AuthenticodeSignature $msi - if ($sig.Status -ne 'Valid') { - throw "Authenticode signature status is '$($sig.Status)' (expected 'Valid')" - } - Write-Host "Signature OK: $($sig.SignerCertificate.Subject)" - - - name: Install new release over previous - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - $msi = (Resolve-Path "dist\PySceneDetect-$env:VERSION-win64.msi").Path - Invoke-Msiexec -MsiArgs @('/i', $msi, '/qn', '/norestart') -LogPath install-upgrade.log - - - name: Verify upgrade - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - $visible = @(Get-VisiblePySceneDetectEntries) - if ($visible.Count -ne 1) { - Get-PySceneDetectEntries | Format-List | Out-String | Write-Host - throw "Expected exactly 1 visible uninstall entry after upgrade, found $($visible.Count)" - } - $entry = $visible[0] - if ($entry.DisplayVersion -ne $env:MSI_VERSION) { - throw "DisplayVersion is '$($entry.DisplayVersion)', expected '$env:MSI_VERSION'" - } - # The visible key name embeds the version ("PySceneDetect - # "), so the old key vanishing from every hive is a - # precise duplicate-Apps-&-Features-entry check. - $stale = @(Get-UninstallKeyByName -KeyName "PySceneDetect $env:PREV_MSI_VERSION") - if ($stale.Count -ne 0) { - throw "Previous version's uninstall key still present: $($stale -join ', ')" - } - # Belt and braces: no entry anywhere (visible or hidden) may still - # report the old version. - $old = @(Get-PySceneDetectEntries | Where-Object { $_.DisplayVersion -eq $env:PREV_MSI_VERSION }) - if ($old.Count -ne 0) { - $old | Format-List | Out-String | Write-Host - throw "Found $($old.Count) uninstall entries still at $env:PREV_MSI_VERSION" - } - if (-not $entry.InstallLocation) { throw 'InstallLocation is empty after upgrade' } - $exe = Join-Path $entry.InstallLocation 'scenedetect.exe' - if (-not (Test-Path $exe)) { throw "scenedetect.exe not found in $($entry.InstallLocation)" } - $out = & $exe version 2>&1 | Out-String - Write-Host $out - if ($LASTEXITCODE -ne 0) { throw "scenedetect version exited with $LASTEXITCODE" } - if (-not $out.Contains($env:VERSION)) { throw "version output does not mention $env:VERSION" } - # If the upgrade relocated the install, the old copy must be gone. - if ($env:OLD_INSTALL_DIR.TrimEnd('\') -ne $entry.InstallLocation.TrimEnd('\')) { - if (Test-Path (Join-Path $env:OLD_INSTALL_DIR 'scenedetect.exe')) { - throw "Old install at $env:OLD_INSTALL_DIR still present after relocating upgrade" - } - } - Add-Content $env:GITHUB_ENV "INSTALL_DIR=$($entry.InstallLocation)" - Write-Host 'Upgrade verified.' - - - name: Uninstall new MSI - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - $msi = (Resolve-Path "dist\PySceneDetect-$env:VERSION-win64.msi").Path - Invoke-Msiexec -MsiArgs @('/x', $msi, '/qn', '/norestart') -LogPath uninstall.log - - - name: Verify clean removal - run: | - . (Join-Path $env:RUNNER_TEMP 'installer-helpers.ps1') - Assert-CleanRemoval -InstallDir $env:INSTALL_DIR - - - name: Upload msiexec logs - if: failure() - uses: actions/upload-artifact@v6 - with: - name: upgrade-logs - path: '*.log' - if-no-files-found: ignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b2f656fa..00000000 --- a/.gitignore +++ /dev/null @@ -1,98 +0,0 @@ -docs/_build/ -docs/STYLE.md -website/build/ -scripts/local/ -tests/resources/* -*.mp4 -*.jpg -*.jpeg -*.patch -*.exe -*.mkv -*.m4v -*.csv -*.txt - -benchmark/BBC/ -benchmark/AutoShot/ -benchmark/ClipShots/ -benchmark/results/ - -packaging/windows/.version_info -packaging/windows/installer/PySceneDetect.back*.aip -packaging/windows/installer/PySceneDetect-*.msi -packaging/windows/installer/PySceneDetect-cache/ - -# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore - -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST -*.manifest -*.spec -pip-log.txt -pip-delete-this-directory.txt -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ -*.mo -*.pot -.scrapy -docs/_build/ -.pybuilder/ -target/ -.ipynb_checkpoints -profile_default/ -ipython_config.py -.pdm.toml -__pypackages__/ -celerybeat-schedule -celerybeat.pid -*.sage.py -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ -.spyderproject -.spyproject -.ropeproject -/site -.mypy_cache/ -.dmypy.json -dmypy.json -.pyre/ -.pytype/ -cython_debug/ -test_clips/ diff --git a/benchmark/AutoShot/.gitkeep b/.nojekyll similarity index 100% rename from benchmark/AutoShot/.gitkeep rename to .nojekyll diff --git a/404.html b/404.html new file mode 100644 index 00000000..f2d80df2 --- /dev/null +++ b/404.html @@ -0,0 +1,176 @@ + + + + + + + + PySceneDetect + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • »
  • +
  • +
  • +
+
+
+
+
+ +

Page Not Found

+

This page does not exist.

+ + + +
+
+ +
+ +
+ +

Copyright © 2014 Brandon Castellano. All rights reserved.
Licensed under BSD 3-Clause (see the LICENSE file for details).

+
+ + Built with MkDocs using a theme provided by Read the Docs. +
+ +
+
+ +
+ +
+ +
+ + + + + +
+ + + + + + + + + + diff --git a/CITATION.cff b/CITATION.cff deleted file mode 100644 index 748d4dbf..00000000 --- a/CITATION.cff +++ /dev/null @@ -1,12 +0,0 @@ -cff-version: 1.2.0 -title: PySceneDetect -message: www.scenedetect.com -type: software -authors: - - given-names: Brandon - family-names: Castellano - affiliation: www.bcastell.com -repository-code: 'https://github.com/Breakthrough/PySceneDetect' -url: 'https://www.scenedetect.com' -abstract: Video Cut Detection and Analysis Tool -license: BSD-3-Clause diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..8179ff95 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +www.scenedetect.com \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 039790f3..00000000 --- a/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2014-2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# - -FROM python:3.11.11-slim - -# Create a non-root user for security hardening -RUN useradd -m scenedetect - -# Set working directory and copy files with correct ownership -WORKDIR /app -COPY --chown=scenedetect:scenedetect . . - -# Install necessary system dependencies as root first -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - ffmpeg \ - mkvtoolnix && \ - rm -rf /var/lib/apt/lists/* - -# Install the scenedetect-headless variant: full program (CLI + opencv-python-headless) -# with the optional media backends. The repo root pyproject builds scenedetect-core -# (library only, no CLI), so swap in the headless variant pyproject first. -# pyav is highly recommended for faster/more robust video decodes -# moviepy provides an alternative video splitting backend -RUN --mount=type=cache,target=/root/.cache/pip \ - cp packaging/variants/pyproject-scenedetect-headless.toml pyproject.toml && \ - pip install ".[pyav,moviepy]" && \ - # TODO(https://github.com/Zulko/moviepy/issues/2553): moviepy caps pillow<12.0, but 11.x has - # CVEs only fixed in 12.3.0+. Tests pass against 12.3.0; drop this once moviepy lifts the cap. - pip install "pillow==12.3.0" - -# Switch to the non-root user -USER scenedetect - -# The default behavior is to run the CLI -ENTRYPOINT ["scenedetect"] - diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c03985e6..00000000 --- a/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (C) 2014, Brandon Castellano - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index cf223d6b..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,12 +0,0 @@ -recursive-exclude .github * -recursive-exclude packaging * -recursive-exclude scripts * -recursive-exclude docs * -recursive-exclude website * -exclude * -include README.md -include LICENSE -include pyproject.toml -include scenedetect.cfg -include packaging/package-info.rst -recursive-include docs * diff --git a/README.md b/README.md deleted file mode 100644 index 4ce8d4ca..00000000 --- a/README.md +++ /dev/null @@ -1,137 +0,0 @@ - - - - PySceneDetect - - -# Video Cut Detection and Analysis Tool - -[![Build Status](https://img.shields.io/github/actions/workflow/status/Breakthrough/PySceneDetect/build.yml)](https://github.com/Breakthrough/PySceneDetect/actions) -[![PyPI Status](https://img.shields.io/pypi/status/scenedetect.svg)](https://pypi.python.org/pypi/scenedetect/) -[![PyPI Version](https://img.shields.io/pypi/v/scenedetect?color=blue)](https://pypi.python.org/pypi/scenedetect/) -[![PyPI License](https://img.shields.io/pypi/l/scenedetect.svg)](https://scenedetect.com/copyright/) - ----------------------------------------------------------- - -### Latest Release: v0.7.1 (July 21, 2026) - -**Website**: [scenedetect.com](https://www.scenedetect.com) - -**Quickstart Example**: [scenedetect.com/cli/](https://www.scenedetect.com/cli/) - -**Documentation**: [scenedetect.com/docs/](https://www.scenedetect.com/docs/) - -**Discord**: https://discord.gg/H83HbJngk7 - ----------------------------------------------------------- - -**Quick Install**: - - pip install scenedetect --upgrade - -Requires ffmpeg/mkvmerge for video splitting support. Windows builds (MSI installer/portable ZIP) can be found on [the download page](https://scenedetect.com/download/). A Docker image with all dependencies included is available as [`ghcr.io/breakthrough/pyscenedetect`](https://github.com/Breakthrough/PySceneDetect/pkgs/container/pyscenedetect). - ----------------------------------------------------------- - -**Quick Start (Command Line)**: - -Split input video on each fast cut using `ffmpeg`: - - scenedetect -i video.mp4 split-video - -Save some frames from each cut: - - scenedetect -i video.mp4 save-images - -Skip the first 10 seconds of the input video: - - scenedetect -i video.mp4 time -s 10s - -More examples can be found throughout [the documentation](https://www.scenedetect.com/docs/latest/cli.html). - -**Quick Start (Docker)**: - -The same commands work without installing anything using [the official Docker image](https://github.com/Breakthrough/PySceneDetect/pkgs/container/pyscenedetect), which includes all dependencies (`ffmpeg`/`mkvmerge` included). Mount the folder containing your videos and use it for input/output paths: - - docker run --rm -v "$(pwd):/files" ghcr.io/breakthrough/pyscenedetect -i /files/video.mp4 split-video -o /files - -**Quick Start (Python API)**: - -To get started, there is a high level function in the library that performs content-aware scene detection on a video (try it from a Python prompt): - -```python -from scenedetect import detect, ContentDetector -scene_list = detect('my_video.mp4', ContentDetector()) -``` - -`scene_list` will now be a list containing the start/end times of all scenes found in the video. There also exists a two-pass version `AdaptiveDetector` which handles fast camera movement better, and `ThresholdDetector` for handling fade out/fade in events. - -Try calling `print(scene_list)`, or iterating over each scene: - -```python -from scenedetect import detect, ContentDetector -scene_list = detect('my_video.mp4', ContentDetector()) -for i, scene in enumerate(scene_list): - print(' Scene %2d: Start %s / Frame %d, End %s / Frame %d' % ( - i+1, - scene[0].get_timecode(), scene[0].frame_num, - scene[1].get_timecode(), scene[1].frame_num,)) -``` - -We can also split the video into each scene if `ffmpeg` is installed (`mkvmerge` is also supported): - -```python -from scenedetect import detect, ContentDetector, split_video_ffmpeg -scene_list = detect('my_video.mp4', ContentDetector()) -split_video_ffmpeg('my_video.mp4', scene_list) -``` - -For more advanced usage, the API is highly configurable, and can easily integrate with any pipeline. This includes using different detection algorithms, splitting the input video, and much more. The following example shows how to implement a function similar to the above, but using [the `scenedetect` API](https://www.scenedetect.com/docs/latest/api.html): - -```python -from scenedetect import open_video, SceneManager, split_video_ffmpeg -from scenedetect.detectors import ContentDetector -from scenedetect.video_splitter import split_video_ffmpeg - -def split_video_into_scenes(video_path, threshold=27.0): - # Open our video, create a scene manager, and add a detector. - video = open_video(video_path) - scene_manager = SceneManager() - scene_manager.add_detector( - ContentDetector(threshold=threshold)) - scene_manager.detect_scenes(video, show_progress=True) - scene_list = scene_manager.get_scene_list() - split_video_ffmpeg(video_path, scene_list, show_progress=True) -``` - -See [the documentation](https://www.scenedetect.com/docs/latest/api.html) for more examples. - -**Benchmark**: - -We evaluate the performance of different detectors in terms of accuracy and processing speed. See [www.scenedetect.com/benchmarks](https://www.scenedetect.com/benchmarks/) for results, or the [benchmark report](benchmark/README.md) for details on the datasets and methodology. - -## Reference - - - [Documentation](https://www.scenedetect.com/docs/) (covers application and Python API) - - [CLI Example](https://www.scenedetect.com/cli/) - - [Config File](https://www.scenedetect.com/docs/latest/cli/config_file.html) - -## Help & Contributing - -Please submit any bugs/issues or feature requests to [the Issue Tracker](https://github.com/Breakthrough/PySceneDetect/issues). Before submission, ensure you search through existing issues (both open and closed) to avoid creating duplicate entries. -Pull requests are welcome and encouraged. PySceneDetect is released under the BSD 3-Clause license, and submitted code should be compliant. - -For help or other issues, you can join [the official PySceneDetect Discord Server](https://discord.gg/H83HbJngk7), submit an issue/bug report [here on Github](https://github.com/Breakthrough/PySceneDetect/issues), or contact me via [my website](https://bcastell.com/about/). - -## Code Signing - -This program uses free code signing provided by [SignPath.io](https://signpath.io?utm_source=foundation&utm_medium=github&utm_campaign=PySceneDetect), and a free code signing certificate by the [SignPath Foundation](https://signpath.org?utm_source=foundation&utm_medium=github&utm_campaign=PySceneDetect) - -## License - -BSD-3-Clause; see [`LICENSE`](LICENSE) and [`THIRD-PARTY.md`](THIRD-PARTY.md) for details. - ----------------------------------------------------------- - -Copyright (C) 2014 Brandon Castellano. -All rights reserved. diff --git a/RELEASE-PLAN.md b/RELEASE-PLAN.md deleted file mode 100644 index bcd86ce7..00000000 --- a/RELEASE-PLAN.md +++ /dev/null @@ -1,73 +0,0 @@ -# PySceneDetect Release Checklist - -Use one copy per release, copy into a pull request and check each box as steps are completed. -Optional: version referenced below as `X.Y[.Z]` - replace with the real version throughout. - -## 1. Version Identifiers, Branch Prep - -- [ ] Create release branch `releases/X.Y[.Z]` off `main` (each release, including patches, gets its own branch - e.g. `releases/0.6.7`, `releases/0.7.1`); fast-forward it to `main` as release work lands. -- [ ] Bump `__version__` in `scenedetect/__init__.py` -- [ ] Bump `docs/LATEST_VERSION` for any stable release: it must match the `releases/X.Y[.Z]` branch suffix for `generate-docs.yml` to update `docs/latest` -- [ ] Regular release: No `-dev` suffix or other, pre-release: has suffix `-dev0`, `-dev1`, ... - -## 2. Documentation, Website, Changelog - -- [ ] Docstrings / API docs reflect any signature changes (`cd docs/ && make html` builds clean). -- [ ] `docs/api/migration_guide.rst` updated if any public API changed. -- [ ] Docstring examples all run correctly, nothing references removed or deprecated symbols. -- [ ] Changelog has release notes for major/minor release, all features, breaking changes, bug fixes, and known issues are documented. -- [ ] `website/pages/download.md` updated with the new version / installer link / release date. -- [ ] `website/pages/changelog.md`: move the release changes from the **Development** section at the bottom to the top. -- [ ] `website/pages/index.md`: Latest release version and date updated. - -## 3. Tests - -- [ ] Static analysis passing (ruff + pyright). -- [ ] Unit tests green locally and in CI: `pytest -vv` (should collect `-m 'not release'` by default). -- [ ] Release test suite green: manually trigger or make a release candidate tag, all 4 jobs (`static`, `release-tests`, `install-matrix`, `long-stress`) green across the OS and Python version matrix. -- [ ] `pip-audit` clean (or exceptions documented in the changelog). - -## 4. Prepare Windows Distribution - -- [ ] Update `packaging/windows/requirements.txt` and bump bundled ffmpeg version in `appveyor.yml` -- [ ] Run AppVeyor build on release branch, ensure resulting portable distribution and MSI installer are correct - -> **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`. -> - Adding/removing build configurations, features, or prerequisites. -> - Install directory layout (`APPDIR` location), or per-component attributes. -> - Editing dialog layouts, branding bitmaps, install sequences, custom actions, file associations, or shortcuts. - -## 5. Tag & Draft Release - -- [ ] Final commit on `releases/X.Y[.Z]`: "Release vX.Y[.Z]". -- [ ] Tag `vX.Y[.Z]` on that commit and push (the legacy `vX.Y[.Z]-release` form is also accepted by all workflows). Wait for all tests/builds to pass. -- [ ] Approve code signing request on SignPath, download `scenedetect-signed.zip` -- [ ] Finalize Windows artifacts locally (CI can't do this - signing happens after the AppVeyor build, so the post-signing steps must run locally): - - Create `dist/signed/` and drop `scenedetect-signed.zip` (from SignPath) into it. No other inputs needed - the portable .zip is rebuilt from the signed .msi via `msiexec /a`, eliminating the AppVeyor download. - - Run `python scripts/finalize_windows_dist.py`. This extracts the signed `.msi` from the bundle, runs `msiexec /a` to recover the installed file tree, repacks it as the portable `.zip` with 7-Zip, writes `PySceneDetect-X.Y.Z-win64.manifest.json` + `SHA256SUMS`, and then runs `scripts/validate_release.py` to verify filenames, hashes, Authenticode signatures, MSI/zip parity, and frozen `.exe` smoke tests. -- [ ] Draft release on Github using the tagged commit: include full changelog & release notes, signed portable .ZIP, signed .MSI installer, Python .whl/.tar.gz packages, and checksum manifests (`PySceneDetect-X.Y.Z-win64.manifest.json` + `SHA256SUMS`) -- [ ] Verify all artifacts uploaded to Github release are valid and named correctly -- [ ] Smoke-test all release artifacts - -## 6. Publish & Release Checks - -- [ ] Dispatch `release.yml` (Release Orchestrator) with the release tag while the Github release is still a **draft**. It runs the verify-then-publish ladder (MSI install/upgrade test -> TestPyPI -> publish Github release -> PyPI -> Docker), verifying each stage before the next; `verify-only` stops before anything goes public. See the header of `release.yml` for details. -- [ ] Verify both projects: https://pypi.org/project/scenedetect/ and https://pypi.org/project/scenedetect-headless/. -- [ ] Deploy website: `generate-website.yml` -- [ ] Deploy docs: `generate-docs.yml` -- [ ] Merge release branch back into `main`, verify `docs/LATEST_VERSION` is correct -- [ ] [Manually dispatch `generate-docs.yml`](https://github.com/Breakthrough/PySceneDetect/actions/workflows/generate-docs.yml) against `releases/X.Y` to update www.scenedetect.com/docs/latest -- [ ] Smoke-test PyPI release: in a fresh venv, `pip install scenedetect==X.Y.Z`; CLI launches and `scenedetect --version` looks correct. -- [ ] Verify download links on website are correct, PyPI project page is up to date and correct. -- [ ] Clear / archive release-scoped tracking files (`tracking.md`, any release-specific TODOs). -- [ ] Announce: project site, relevant issues / discussions closed and linked to the release. - ---- - -## Notes - -- **Branching model**: work spans multiple commits on `releases/X.Y[.Z]`; the final one gets the `vX.Y[.Z]` tag which gates the release-test workflow. A passing release-test is a hard prerequisite for publishing. -- **Version consistency** is enforced in two places (`__init__.py`, `PySceneDetect.aip`). The `static` job of `release-test.yml` checks `__init__.py` against the tag and verifies the changelog has a matching `## PySceneDetect X.Y` heading; the installer parity is checked by `scripts/pre_release.py --release`. -- **Changelog convention**: the in-development section lives at the *bottom* of `website/pages/changelog.md` under the "Development" heading - don't move it to the top. diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md deleted file mode 100644 index 959c2e4f..00000000 --- a/THIRD-PARTY.md +++ /dev/null @@ -1,75 +0,0 @@ -# Ancillary Software Licenses - -This file includes license information for various open-source projects -that are imported, derived into, or distributed with PySceneDetect. -See [LICENSE](LICENSE) for the main PySceneDetect license. - -Depending on the features being used, PySceneDetect uses the following -third-party software which are released under the terms detailed below. -By downloading, copying, installing or using this software, you agree -to these terms. - -In no particular order: - ------------------------------------------------------------------------ - -> click [Copyright (C) 2017, Armin Ronacher]: - This software uses OpenCV; see thirdparty/LICENSE-CLICK or visit: - [ http://click.pocoo.org/license/ ] - -> NumPy [Copyright (C) 2005-2016, Numpy Developers]: - This software uses Numpy; see thirdparty/LICENSE-NUMPY or visit: - [ http://www.numpy.org/license.html ] - -> OpenCV [Copyright (C) 2017, Itseez]: - This software uses OpenCV; see thirdparty/LICENSE-OPENCV or visit: - [ http://opencv.org/license.html ] - -> PyAV [Copyright (C) 2017, Mike Boers and others]: - This software uses PyAV; see thirdparty/LICENSE-PYAV or visit: - [ https://github.com/PyAV-Org/PyAV/blob/main/LICENSE.txt ] - -> pytest [Copyright (C) 2004-2017, Holger Krekel and others]: - This software uses pytest; see thirdparty/LICENSE-PYTEST or visit: - [ https://docs.pytest.org/en/latest/license.html ] - -> simpletable [Copyright (C) 2014-2019, Matheus Vieira Portela and others]: - This software uses simpletable; see thirdparty/LICENSE-SIMPLETABLE or visit: - [ https://github.com/matheusportela/simpletable/blob/master/LICENSE ] - -> tqdm [Copyright (C) 2013-2018, Casper da Costa-Luis, - Google Inc., and Noam Yorav-Raphael]: - This software uses tqdm; see thirdparty/LICENSE-TQDM or visit: - [ https://github.com/tqdm/tqdm/blob/master/LICENCE ] - -> MoviePy [ Copyright (C) 2015 Zulko ] - This software uses tqdm; see thirdparty/LICENSE-TQDM or visit: - [ https://github.com/Zulko/moviepy/blob/master/LICENCE.txt ] - ------------------------------------------------------------------------ - -This software may also invoke FFmpeg or mkvmerge, if available. If required, -these programs can be obtained from following URLs: - - FFmpeg: [ https://ffmpeg.org/download.html ] - mkvmerge: [ https://mkvtoolnix.download/downloads.html ] - -Once installed, ensure the program is in your PATH variable (i.e. you can -run the `ffmpeg` or `mkvmerge` command from any location). - -Certain distributions of PySceneDetect may include ffmpeg. See -thirdparty/LICENSE-FFMPEG file or visit [ https://ffmpeg.org ] - -FFmpeg is a trademark of Fabrice Bellard -mkvmerge is Copyright (C) 2005-2016, Matroska - -Windows distributions may include a compiled Python distribution. For license -information regarding the distributed version of Python, see the -thirdparty/LICENSE-PYTHON file, or visit [ https://docs.python.org/3/license.html ] - ------------------------------------------------------------------------ - -If any information above is incorrect, please let us know. -Visit [ https://www.scenedetect.com ] for contact information. - - diff --git a/api/index.html b/api/index.html new file mode 100644 index 00000000..13fafeab --- /dev/null +++ b/api/index.html @@ -0,0 +1,223 @@ + + + + + + + + Python API - PySceneDetect + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+
+
+ +

API Reference

+

The Python API is documented using Sphinx and can be found here.

+

Scene Detection Algorithms

+

This page discusses the scene detection methods/algorithms available for use in PySceneDetect, including details describing the operation of the detection method, as well as relevant command-line arguments and recommended values.

+

Content-Aware Detector

+

The content-aware scene detector (detect-content) detects jump cuts in the input video. This is typically what people think of as "cuts" between scenes in a movie - given two adjacent frames, do they belong to the same scene? The content-aware scene detector finds areas where the difference between two subsequent frames exceeds the threshold value that is set (a good value to start with is --threshold 27).

+

Internally, this detector functions by converting the colorspace of each decoded frame from RGB into HSV. It then takes the average difference across all channels (or optionally just the value channel) from frame to frame. When this exceeds a set threshold, a scene change is triggered.

+

detect-content also has edge detection, which can be enabled by providing a set of 4 numbers in the form (delta_hue, delta_sat, delta_lum, delta_edges). Changes in edges are typically larger than the other components, so threshold may need to be increased accordingly. For example, -w 1.0 0.5 1.0 0.2 -t 32 is a good starting point to use with edge detection. The default weights are --weights 1.0 1.0 1.0 0.0 which does not include edges, but this may change in the future.

+

See the documentation for detect-content for details.

+

Adaptive Content Detector

+

The adaptive content detector (detect-adaptive) compares the difference in content between adjacent frames similar to detect-content but instead using a rolling average of adjacent frame changes. This helps mitigate false detections where there is fast camera motion.

+

Threshold Detector

+

The threshold-based scene detector (detect-threshold) is how most traditional scene detection methods work (e.g. the ffmpeg blackframe filter), by comparing the intensity/brightness of the current frame with a set threshold, and triggering a scene cut/break when this value crosses the threshold. In PySceneDetect, this value is computed by averaging the R, G, and B values for every pixel in the frame, yielding a single floating point number representing the average pixel value (from 0.0 to 255.0).

+

Histogram Detector

+

The scene change detection algorithm uses histograms of the Y channel in the YCbCr color space to detect scene changes, which helps mitigate issues caused by lighting variations. Each frame of the video is converted from its original color space to the YCbCr color space. The Y channel, which represents luminance, is extracted from the YCbCr color space. This helps in focusing on intensity variations rather than color variations. A histogram of the Y channel is computed using the specified number of bins (--bins/-b). The histogram is normalized to ensure that it can be consistently compared with histograms from other frames. The normalized histogram of the current frame is compared with the normalized histogram of the previous frame using the correlation method (cv2.HISTCMP_CORREL). A scene change is detected if the correlation between the histograms of consecutive frames is below the specified threshold (--threshold/-t). This indicates a significant change in luminance, suggesting a scene change.

+

Perceptual Hash Detector

+

The perceptual hash detector (detect-hash) calculates a hash for a frame and compares that hash to the previous frame's hash. If the hashes differ by more than the defined threshold, then a scene change is recorded. The hashing algorithm used for this detector is an implementation of phash from the imagehash library. In practice, this detector works similarly to detect-content in that it picks up large differences between adjacent frames. One important note is that the hashing algorithm converts the frames to grayscale, so this detector is insensitive to changes in colors if the brightness remains constant. In general, this algorithm is very computationally efficient compared to detect-content or detect-adaptive, especially if downscaling is not used. See here for an overview of how a perceptual hashing algorithm can be used for detecting similarity (or otherwise) of images and a visual depiction of the algorithm.

+

Creating New Detection Algorithms

+

All scene detection algorithms must inherit from the base SceneDetector class. Note that the current SceneDetector API is under development and expected to change somewhat before v1.0 is released, so make sure to pin your scenedetect dependency to the correct API version (e.g. scenedetect < 0.6, scenedetect < 0.7, etc...).

+

Creating a new scene detection method can be as simple as implementing the process_frame function, and optionally post_process:

+
import typing as ty
+import numpy as np
+from scenedetect import FrameTimecode, SceneDetector
+
+class CustomDetector(SceneDetector):
+    """CustomDetector class to implement a scene detection algorithm."""
+
+    def process_frame(
+        self,
+        timecode: FrameTimecode,
+        frame_im: np.ndarray,
+    ) -> ty.List[FrameTimecode]:
+        # Return a list of timecodes where we found cuts (either on this frame or previously).
+        return []
+
+    def post_process(self, timecode: FrameTimecode) -> ty.List[FrameTimecode]:
+        # Called after the last frame has been read to handle pending events.
+        return []
+
+

process_frame is called on every frame in the input video, which will be called after the final frame of the video is passed to process_frame. This may be useful for multi-pass algorithms, or detectors which are waiting on some condition but still wish to output an event on the final frame.

+

For example, a detector may output at most 1 cuts for every call to process_frame, it may output the entire scene list in post_process, or a combination of both. Note that the latter will not work in cases where a live video stream or camera input device is being used. See the API documentation for the SceneDetector class for details. Alternatively, you can call help(SceneDetector) from a Python REPL. For examples of actual detection algorithm implementations, see the source files in the scenedetect/detectors/ directory (e.g. threshold_detector.py, content_detector.py).

+

Processing is done by calling the process_frame(...) function for all frames in the video, followed by post_process(...) (optional) after the final frame. Scene cuts are detected and added to the passed list object in both cases.

+

process_frame(...) is called for each frame in sequence, passing the following arguments:

+
    +
  • frame_num: the number of the current frame being processed
  • +
  • frame_img: frame returned video file or stream (accessible as NumPy array)
  • +
  • frame_metrics: dictionary for memoizing results of detection algorithm calculations for quicker subsequent analyses (if possible)
  • +
  • scene_list: List containing the frame numbers where all scene cuts/breaks occur in the video.
  • +
+

post_process(...) is called after the final frame has been processed, to allow for any stored scene cuts to be written if required (e.g. in the case of the ThresholdDetector).

+

You may also want to look into the implementation of current detectors to understand how frame metrics are saved/loaded to/from a StatsManager for caching and allowing values to be written to a stats file for users to graph and find trends in to tweak detector options. Also see the documentation for the SceneManager for details.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + + diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 0ffafc7c..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,156 +0,0 @@ -# Build signed releases for PySceneDetect Windows x64 - -build: false - -cache: - # FFmpeg self-invalidates via %ffmpeg_version% in the filename; AdvInst MSI and - # Inkscape rarely need refresh and have `if not exist` install guards, so we - # don't tie them to appveyor.yml (any edit there would force a cold-cache - # reinstall of all three and blow past the 10-minute build limit). - - 'ffmpeg-%ffmpeg_version%-full_build.7z' - - 'packaging\windows\installer\advinst.msi' - - '%LOCALAPPDATA%\uv\cache -> pyproject.toml' - - 'C:\Program Files\Inkscape' - -# Branches applies to tags as well. We only build on tagged releases of the form -# vX.Y[.Z] (the legacy -release suffix is also accepted, matching the GitHub workflows). -branches: - only: - - main - - /releases\/.+/ - - /v.+/ - -skip_tags: false -skip_non_tags: true - -environment: - matrix: - - PYTHON: "C:\\Python313-x64" - # Encrypted AdvancedInstaller License - ai_license_secret: - secure: QRCPoNYF1nqgXDn7pHgBzg== - ai_license_salt: - secure: +Gy+SRk8JUsaM+5pMEKITiJxdLilrxHpkKlrZzR3C9DPwdgYLGxt5sJn6uXuAJg7e6JsKHcT7tRks/HcSKkHPw== - ffmpeg_version: "8.1.2" - -# SignPath Config for Code Signing -deploy: -- provider: Webhook - url: https://app.signpath.io/API/v1/f2efa44c-5b5c-45f2-b44f-8f9dde708313/Integrations/AppVeyor?ProjectSlug=PySceneDetect&SigningPolicySlug=release-signing - authorization: - secure: FBgWCaxCCKOqc2spYf5NGWSNUGLbT5WeuC5U0k4Of1Ids9n51YWxhGlMyzLbdNBFe64RUcOSzk/N3emlQzbsJg== - on: - APPVEYOR_REPO_TAG: true # keep casing this way for Linux builds where variables are case-sensitive - -install: - - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - - echo * * SETTING UP PYTHON ENVIRONMENT * * - - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - - 'SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%' - - python --version - - python -m pip install uv - - uv pip install --system .[docs] - - uv pip install --system -r packaging/windows/requirements.txt --no-binary imageio-ffmpeg - # TODO(https://github.com/Zulko/moviepy/issues/2553): moviepy caps pillow<12.0, but 11.x has - # CVEs only fixed in 12.3.0+. Installed as a separate step since a pin in requirements.txt - # would fail strict resolution against moviepy's constraint. Tests pass against 12.3.0; - # drop this once moviepy lifts the cap. - - uv pip install --system pillow==12.3.0 - - 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. 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 - - python scripts/stage_windows_dist.py --ffmpeg-dir dist/ffmpeg - - - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - - echo * * BUILDING MSI INSTALLER * * - - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - # Download, install, and register AdvancedInstaller - - 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% - - if not exist advinst.msi appveyor DownloadFile https://www.advancedinstaller.com/downloads/advinst.msi - - msiexec /i advinst.msi /qn - # 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 packaging/windows/installer/PySceneDetect.aip - - - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - - echo * * PACKAGING BUILD ARTIFACTS * * - - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - # Zip all resources together for code signing - - move packaging\windows\installer\PySceneDetect-*.msi dist\ - - cd dist - - cp scenedetect\scenedetect.exe . - - 7z a scenedetect-signed.zip scenedetect.exe PySceneDetect-*.msi - - cd .. - -test_script: - - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - - echo * * TESTING BUILD * * - - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - # Checkout required test resources - - git fetch --depth=1 https://github.com/Breakthrough/PySceneDetect.git refs/heads/resources:refs/remotes/origin/resources - - git checkout refs/remotes/origin/resources -- tests/resources/ - - move dist\scenedetect\ffmpeg.exe ffmpeg.exe - # Run unit tests - # TODO: We are at the new build time limit for this plan apparently, 10 mins. Figure out a - # strategy to deal with that (see if we can use Github as a builder?). - # - pytest - # Test Windows build - - move ffmpeg.exe dist\scenedetect\ffmpeg.exe - - cd dist/scenedetect - - scenedetect.exe version - - scenedetect.exe -i ../../tests/resources/testvideo.mp4 -b opencv detect-content time -e 2s - - scenedetect.exe -i ../../tests/resources/testvideo.mp4 -b pyav detect-content time -e 2s - -artifacts: - # Portable ZIP (named PySceneDetect-X.Y.Z-win64.zip by stage_windows_dist.py) - - path: dist/PySceneDetect-*-win64.zip - name: PySceneDetect-win64 - # 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-*.manifest.txt - name: PySceneDetect-build-manifest-payload diff --git a/benchmark/BBC/.gitkeep b/benchmark/BBC/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/benchmark/README.md b/benchmark/README.md deleted file mode 100644 index 180b0d77..00000000 --- a/benchmark/README.md +++ /dev/null @@ -1,194 +0,0 @@ -# Benchmarking PySceneDetect - -This page contains the results of benchmarking PySceneDetect's detection accuracy againts public -shot-boundary-detection datasets. Scoring follows the [TRECVID-SBD][trecvid] convention -(greedy 1-to-1 nearest-neighbor matching, with a configurable frame tolerance for hard cuts; -point-in-interval matching for fade transitions; mean absolute frame offset on matched events) -so numbers are comparable to published SBD results. - -[trecvid]: https://www-nlpir.nist.gov/projects/tv2007/pastdata/shot_boundary.07.html - -Supported datasets: - -- [BBC Planet Earth](https://zenodo.org/records/14865504): - 11 long-form broadcast clips; hard cuts only -- [AutoShot](https://drive.google.com/file/d/17diRkLlNUUjHDooXdqFUTXYje2-x4Yt6/view?usp=sharing): - Short-form web clips; hard cuts only -- [ClipShots](https://github.com/Tangshitao/ClipShots): - Short-form web clips; hard cuts and typed gradual transitions (fades/dissolves) - -## Usage - -```bash -# Single detector x single dataset: -python -m benchmark --detector detect-content --dataset BBC -``` - -Pass `--help` for `--dataset-root`, `--backend`, `--tolerance`, and `--out` options. - -### Parameter sweeps - -`python -m benchmark.sweep` runs a grid over detector parameters and reports the -top cells by F1 plus the Pareto front across tolerances. One decode is shared by up to -`--workers` parallel detectors via an internal fan-out wrapper, so the cost scales with -`ceil(cells / workers)` decodes per video rather than `cells` decodes. - -```bash -python -m benchmark.sweep \ - --detector detect-content --dataset BBC \ - --params "threshold=15:35:1;min_scene_len=0.0:1.0:0.1" \ - --tolerance 0,1 --workers 16 \ - --out sweep-content-bbc.json -``` - -`--params`: assignments joined by `;`. Each is either `key=v1,v2,v3` (enumerated values) or -`key=start:stop:step` (numeric range, inclusive when `stop` lands on a step). Omitted keys -use the detector's default. - -Time-valued kwargs (`min_scene_len`, etc.) accept `TimecodeLike` - integers are -frames, floats are seconds, and strings like `"0.1s"` / `"00:00:00.500"` also work. -Prefer floats so the same sweep is comparable across datasets with different -framerates. Use `--quick N` to limit to the first N samples for iteration; published -numbers should always come from the full corpus. - -## Dataset Download - -### BBC - -```bash -# annotations -wget -O BBC/fixed.zip https://zenodo.org/records/14873790/files/fixed.zip -unzip BBC/fixed.zip -d BBC -rm -rf BBC/fixed.zip - -# videos -wget -O BBC/videos.zip https://zenodo.org/records/14873790/files/videos.zip -unzip BBC/videos.zip -d BBC -rm -rf BBC/videos.zip -``` - -### AutoShot - -Download `AutoShot_test.tar.gz` from -[Google Drive](https://drive.google.com/file/d/17diRkLlNUUjHDooXdqFUTXYje2-x4Yt6/view?usp=sharing). - -```bash -tar -zxvf AutoShot_test.tar.gz -rm AutoShot_test.tar.gz -``` - -### ClipShots - -ClipShots is gated behind a dataset request form; direct `wget`-style download links are not -published. See [the download instructions](https://github.com/Tangshitao/ClipShots#downloads) to -obtain the annotations and videos. The expected on-disk layout is: - -``` -ClipShots/ - annotations/{train,test,only_gradual}.json - video_lists/{train,test,only_gradual}.txt - videos/*.mp4 -``` - -The loader defaults to the test split (500 videos). The full corpus is ~46 GB. - -Set `--dataset-root /path/to/datasets` to override. The default dataset location assumes they are -all placed in the benchmark folder (e.g. `benchmark/BBC`, `benchmark/AutoShot`, `benchmark/ClipShots`). - -## Results (defaults) - -Generated by `scripts/benchmark_defaults.sh` at `tolerance=0` (frame-exact matching). -Elapsed is mean wall-clock seconds per video. - -#### BBC - -| Detector | Recall | Precision | F1 | Mean s/video | -|:-----------------:|:------:|:---------:|:-----:|:------------:| -| AdaptiveDetector | 87.12 | 96.55 | 91.59 | 36.12 | -| ContentDetector | 84.70 | 88.77 | 86.69 | 37.02 | -| HashDetector | 92.30 | 75.56 | 83.10 | 25.51 | -| HistogramDetector | 89.84 | 72.03 | 79.96 | 22.29 | -| ThresholdDetector | 0.06 | 0.70 | 0.11 | 16.05 | - -#### AutoShot - -| Detector | Recall | Precision | F1 | Mean s/video | -|:-----------------:|:------:|:---------:|:-----:|:------------:| -| AdaptiveDetector | 70.59 | 77.46 | 73.86 | 3.52 | -| ContentDetector | 63.49 | 76.19 | 69.26 | 4.80 | -| HashDetector | 56.48 | 76.11 | 64.84 | 4.14 | -| HistogramDetector | 63.27 | 53.23 | 57.82 | 3.76 | -| ThresholdDetector | 0.75 | 38.64 | 1.47 | 3.28 | - -#### ClipShots (hard cuts) - -| Detector | Recall | Precision | F1 | Mean s/video | -|:-----------------:|:------:|:---------:|:-----:|:------------:| -| AdaptiveDetector | 85.97 | 41.25 | 55.75 | 1.81 | -| ContentDetector | 81.93 | 42.36 | 55.84 | 2.52 | -| HashDetector | 81.34 | 30.14 | 43.98 | 1.04 | -| HistogramDetector | 72.20 | 11.47 | 19.80 | 0.71 | -| ThresholdDetector | 0.08 | 0.58 | 0.14 | 0.64 | - -#### ClipShots (fades) - -| Detector | Recall | Precision | F1 | -|:-----------------:|:------:|:---------:|:-----:| -| AdaptiveDetector | 13.65 | 98.12 | 23.96 | -| ContentDetector | 26.03 | 98.04 | 41.14 | -| HashDetector | 18.77 | 94.53 | 31.33 | -| HistogramDetector | 69.67 | 81.99 | 75.33 | -| ThresholdDetector | 5.69 | 99.24 | 10.77 | - -## Parameter sweep results - -The tables above use each detector's v0.7 defaults. A grid sweep over the key parameters -scored by hard-cut F1 at 1-frame tolerance, averaged across BBC / AutoShot / ClipShots gives the -best single parameter set for this corpus mix: - -| Detector | Best mean F1 | Best params | v0.7 default | -|:-----------------:|:------------:|:-----------------------------------------------------------|:---------------------------------------| -| ContentDetector | 73.4 | threshold=31, min_scene_len=0.6s | threshold=27 | -| AdaptiveDetector | 76.3 | adaptive_threshold=3.5, window_width=3, min_scene_len=0.6s | adaptive_threshold=3.0, window_width=2 | -| HashDetector | 69.8 | threshold=0.35, size=8 | threshold=0.395, size=16 | -| HistogramDetector | 66.3 | threshold=0.20, bins=128 | threshold=0.05, bins=256 | -| ThresholdDetector | -- | detects fades, not hard cuts (validation only) | threshold=12 | - -Full per-dataset breakdowns are in [`SWEEP_REPORT.md`](SWEEP_REPORT.md), and can be generated -with `python -m benchmark.report_sweep`. The full grids (all detectors and datasets) are driven -by `scripts/benchmark_sweep.sh`. - -## Citations - -### BBC - -``` -@InProceedings{bbc_dataset, - author = {Lorenzo Baraldi and Costantino Grana and Rita Cucchiara}, - title = {A Deep Siamese Network for Scene Detection in Broadcast Videos}, - booktitle = {Proceedings of the 23rd ACM International Conference on Multimedia}, - year = {2015}, -} -``` - -### AutoShot - -``` -@InProceedings{autoshot_dataset, - author = {Wentao Zhu and Yufang Huang and Xiufeng Xie and Wenxian Liu and Jincan Deng and Debing Zhang and Zhangyang Wang and Ji Liu}, - title = {AutoShot: A Short Video Dataset and State-of-the-Art Shot Boundary Detection}, - booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR) Workshops}, - year = {2023}, -} -``` - -### ClipShots - -``` -@InProceedings{clipshots_dataset, - author = {Shitao Tang and Litong Feng and Zhanghui Kuang and Yimin Chen and Wei Zhang}, - title = {Fast Video Shot Transition Localization with Deep Structured Models}, - booktitle = {Asian Conference on Computer Vision (ACCV)}, - year = {2018}, -} -``` diff --git a/benchmark/SWEEP_REPORT.md b/benchmark/SWEEP_REPORT.md deleted file mode 100644 index f368072a..00000000 --- a/benchmark/SWEEP_REPORT.md +++ /dev/null @@ -1,107 +0,0 @@ -# Detector parameter sweep report - -Generated by `benchmark/report_sweep.py` from `benchmark/sweep.py` grid results (hash/hist were swept with `min_scene_len` fixed at its default). F1/precision/recall are percentages on hard cuts; tol = frame tolerance. - -## detect-content - -**Best cell per dataset (by F1@1):** - -| Dataset | F1@1 | Prec@1 | Rec@1 | F1@0 | Params | -| --------- | ----- | ------ | ----- | ----- | ------------------------------- | -| BBC | 88.34 | 90.00 | 86.75 | 87.35 | min_scene_len=0.8, threshold=25 | -| AutoShot | 73.44 | 79.54 | 68.21 | 70.54 | min_scene_len=0.4, threshold=29 | -| ClipShots | 66.74 | 58.93 | 76.95 | 66.44 | min_scene_len=0.8, threshold=35 | - -**Best params averaged across all datasets (mean F1@1):** - -| Mean F1@1 | BBC | AutoShot | ClipShots | Params | -| --------- | ----- | -------- | --------- | ------------------------------- | -| 73.39 | 85.95 | 71.51 | 62.73 | min_scene_len=0.6, threshold=31 | -| 73.37 | 84.27 | 70.95 | 64.89 | min_scene_len=0.6, threshold=33 | -| 73.32 | 83.99 | 72.43 | 63.54 | min_scene_len=0.4, threshold=33 | -| 73.15 | 87.28 | 72.05 | 60.13 | min_scene_len=0.6, threshold=29 | -| 73.09 | 85.18 | 72.97 | 61.10 | min_scene_len=0.4, threshold=31 | - -## detect-adaptive - -**Best cell per dataset (by F1@1):** - -| Dataset | F1@1 | Prec@1 | Rec@1 | F1@0 | Params | -| --------- | ----- | ------ | ----- | ----- | --------------------------------------------------------- | -| BBC | 94.57 | 94.89 | 94.26 | 93.50 | adaptive_threshold=2, min_scene_len=0.6, window_width=2 | -| AutoShot | 77.19 | 80.48 | 74.16 | 75.45 | adaptive_threshold=3.5, min_scene_len=0.4, window_width=3 | -| ClipShots | 65.53 | 60.19 | 71.89 | 65.47 | adaptive_threshold=5.5, min_scene_len=0.6, window_width=3 | - -**Best params averaged across all datasets (mean F1@1):** - -| Mean F1@1 | BBC | AutoShot | ClipShots | Params | -| --------- | ----- | -------- | --------- | --------------------------------------------------------- | -| 76.34 | 90.41 | 76.27 | 62.32 | adaptive_threshold=3.5, min_scene_len=0.6, window_width=3 | -| 76.21 | 87.87 | 76.17 | 64.58 | adaptive_threshold=4, min_scene_len=0.6, window_width=3 | -| 76.18 | 90.43 | 77.19 | 60.93 | adaptive_threshold=3.5, min_scene_len=0.4, window_width=3 | -| 76.16 | 87.91 | 77.07 | 63.51 | adaptive_threshold=4, min_scene_len=0.4, window_width=3 | -| 75.45 | 85.28 | 75.71 | 65.37 | adaptive_threshold=4.5, min_scene_len=0.6, window_width=3 | - -## detect-hash - -**Best cell per dataset (by F1@1):** - -| Dataset | F1@1 | Prec@1 | Rec@1 | F1@0 | Params | -| --------- | ----- | ------ | ----- | ----- | ------------------------ | -| BBC | 86.91 | 81.59 | 92.96 | 85.81 | size=16, threshold=0.425 | -| AutoShot | 70.17 | 76.06 | 65.12 | 66.89 | size=8, threshold=0.325 | -| ClipShots | 56.38 | 44.46 | 77.03 | 55.66 | size=8, threshold=0.4 | - -**Best params averaged across all datasets (mean F1@1):** - -| Mean F1@1 | BBC | AutoShot | ClipShots | Params | -| --------- | ----- | -------- | --------- | ----------------------- | -| 69.83 | 86.38 | 68.98 | 54.12 | size=8, threshold=0.35 | -| 69.63 | 86.65 | 65.86 | 56.38 | size=8, threshold=0.4 | -| 69.63 | 86.65 | 65.86 | 56.37 | size=8, threshold=0.375 | -| 68.18 | 84.28 | 70.17 | 50.10 | size=8, threshold=0.325 | -| 67.00 | 83.71 | 61.66 | 55.65 | size=8, threshold=0.425 | - -## detect-hist - -> Note: thresholds >= 0.21 come from a grid-extension run (`detect-hist-ext-.json`) after the initial grid's best cell landed on its 0.20 upper edge. - -**Best cell per dataset (by F1@1):** - -| Dataset | F1@1 | Prec@1 | Rec@1 | F1@0 | Params | -| --------- | ----- | ------ | ----- | ----- | ------------------------ | -| BBC | 86.58 | 87.32 | 85.86 | 85.42 | bins=128, threshold=0.11 | -| AutoShot | 68.99 | 75.29 | 63.67 | 65.70 | bins=128, threshold=0.2 | -| ClipShots | 53.25 | 46.26 | 62.74 | 52.90 | bins=128, threshold=0.35 | - -**Best params averaged across all datasets (mean F1@1):** - -| Mean F1@1 | BBC | AutoShot | ClipShots | Params | -| --------- | ----- | -------- | --------- | ------------------------ | -| 66.27 | 82.10 | 68.99 | 47.72 | bins=128, threshold=0.2 | -| 66.23 | 81.47 | 68.87 | 48.36 | bins=128, threshold=0.21 | -| 66.20 | 82.74 | 68.81 | 47.06 | bins=128, threshold=0.19 | -| 66.19 | 80.69 | 68.92 | 48.97 | bins=128, threshold=0.22 | -| 66.17 | 79.53 | 68.88 | 50.10 | bins=128, threshold=0.24 | - -## detect-threshold - -> Note: `detect-threshold` detects **fades** (fade to/from black), not hard cuts. These datasets' ground truth is hard cuts, so the hard-cut F1 below is expectedly near zero. It is included to validate the sweep pipeline end-to-end, not as a meaningful hard-cut accuracy result. - -**Best cell per dataset (by F1@1):** - -| Dataset | F1@1 | Prec@1 | Rec@1 | F1@0 | Params | -| --------- | ---- | ------ | ----- | ---- | ------------------------------- | -| BBC | 0.79 | 2.89 | 0.45 | 0.32 | min_scene_len=0.2, threshold=19 | -| AutoShot | 3.98 | 51.09 | 2.07 | 3.14 | min_scene_len=0.4, threshold=20 | -| ClipShots | 1.75 | 6.21 | 1.02 | 0.18 | min_scene_len=0, threshold=10 | - -**Best params averaged across all datasets (mean F1@1):** - -| Mean F1@1 | BBC | AutoShot | ClipShots | Params | -| --------- | ---- | -------- | --------- | ------------------------------- | -| 2.07 | 0.77 | 3.90 | 1.55 | min_scene_len=0, threshold=19 | -| 2.06 | 0.73 | 3.98 | 1.48 | min_scene_len=0, threshold=20 | -| 2.02 | 0.79 | 3.90 | 1.37 | min_scene_len=0.2, threshold=19 | -| 2.00 | 0.71 | 3.98 | 1.30 | min_scene_len=0.2, threshold=20 | -| 1.96 | 0.69 | 3.90 | 1.28 | min_scene_len=0.4, threshold=19 | diff --git a/benchmark/__main__.py b/benchmark/__main__.py deleted file mode 100644 index 2f4f9a3f..00000000 --- a/benchmark/__main__.py +++ /dev/null @@ -1,180 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# -"""Command-line entrypoint for the PySceneDetect benchmark harness. - -Runs one detector against a single dataset using default parameters, and calculates TRECVID-SBD -metrics using the given frame tolerance (usually 0 or 1). Hard-cut precision/recall/F1, mean -absolute frame offset on matches, and per-video elapsed wall-clock are calculated. If a dataset -advertises typed fade ground truth, a second table reports fade precision/recall/F1. -""" - -from __future__ import annotations - -import argparse -import time -from pathlib import Path - -from tqdm import tqdm - -from benchmark._common import ( - DEFAULT_BACKEND, - DETECTORS, - FADE_HEADER, - HARD_HEADER, - fade_row, - hard_row, - parse_tolerances, - render_table, - write_json, -) -from benchmark.dataset import DATASETS, Dataset, resolve_dataset -from benchmark.evaluator import BenchmarkResult, Prediction, evaluate -from scenedetect import AVAILABLE_BACKENDS, detect - - -def _run_predictions( - dataset: Dataset, - detector_name: str, - backend: str, -) -> dict[Path, Prediction]: - """Detect cuts for every video in ``dataset`` and return predictions keyed by path.""" - detector_cls = DETECTORS[detector_name] - predictions: dict[Path, Prediction] = {} - for sample in tqdm(dataset, desc=detector_name): - start = time.time() - pred_scene_list = detect(str(sample.video_file), detector_cls(), backend=backend) - elapsed = time.time() - start - predictions[sample.video_file] = Prediction( - predicted_cuts=[scene[1].frame_num for scene in pred_scene_list], - ground_truth=sample.ground_truth, - elapsed=elapsed, - ) - return predictions - - -def _print_results( - detector: str, - dataset_name: str, - dataset: Dataset, - results: list[BenchmarkResult], -) -> None: - print(f"\n## {detector} on {dataset_name} (hard cuts)\n") - print(render_table(HARD_HEADER, [hard_row(r) for r in results])) - if "fade" in dataset.event_types: - print(f"\n## {detector} on {dataset_name} (fades)\n") - print(render_table(FADE_HEADER, [fade_row(r) for r in results])) - - -# --------------------------------------------------------------------- # -# Entry point -# --------------------------------------------------------------------- # - - -def create_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Benchmarking PySceneDetect performance.") - parser.add_argument( - "--dataset", - type=str, - required=True, - choices=list(DATASETS.keys()), - help=f"Dataset name. One of: {', '.join(DATASETS.keys())}.", - ) - parser.add_argument( - "--detector", - type=str, - required=True, - choices=list(DETECTORS.keys()), - help=f"Detector name. One of: {', '.join(DETECTORS.keys())}.", - ) - parser.add_argument( - "--dataset-root", - type=str, - default=None, - help=( - "Base directory containing per-dataset subfolders. Defaults to 'benchmark' " - "(the in-repo location). Use this to read videos from an external location, " - "e.g. --dataset-root D:/path/to/benchmark." - ), - ) - parser.add_argument( - "--backend", - type=str, - default=DEFAULT_BACKEND, - choices=sorted(AVAILABLE_BACKENDS.keys()), - help=( - f"Video decoding backend (default: {DEFAULT_BACKEND}). Override to compare " - "detector output across backends, e.g. opencv vs pyav." - ), - ) - parser.add_argument( - "--tolerance", - type=str, - default="0,1", - help=( - "Comma-separated list of frame tolerances for hard-cut matching (default: 0,1). " - "+/-0 is the literature-strict number; +/-1 masks single-frame encoder artifacts." - ), - ) - parser.add_argument( - "--out", - type=str, - default=None, - help="Path to write a machine-readable JSON results file (includes per-video stats).", - ) - parser.add_argument( - "--quick", - type=int, - nargs="?", - const=10, - default=None, - metavar="N", - help=( - "Score only the first N samples from the dataset (default N=10) for fast " - "iteration. Use this to smoke-test config changes; published numbers should " - "always come from the full corpus." - ), - ) - return parser - - -def main() -> None: - args = create_parser().parse_args() - tolerances = parse_tolerances(args.tolerance) - dataset = resolve_dataset(args.dataset, args.dataset_root) - if len(dataset) == 0: - raise SystemExit( - f"Dataset {args.dataset!r} at {args.dataset_root or 'benchmark'} is empty - " - "check that videos and annotations are present." - ) - if args.quick is not None: - dataset._samples = dataset._samples[: args.quick] - print(f"--quick: limited to first {len(dataset)} samples") - print(f"Evaluating {args.detector} on {args.dataset} (backend={args.backend})") - - payloads = _run_predictions(dataset, args.detector, args.backend) - results = [evaluate(payloads, tolerance=t) for t in tolerances] - - _print_results(args.detector, args.dataset, dataset, results) - if args.out: - write_json( - args.out, - { - "detector": args.detector, - "dataset": args.dataset, - "backend": args.backend, - "results": [r.to_dict() for r in results], - }, - ) - - -if __name__ == "__main__": - main() diff --git a/benchmark/_common.py b/benchmark/_common.py deleted file mode 100644 index c175dcc0..00000000 --- a/benchmark/_common.py +++ /dev/null @@ -1,104 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# -"""Shared formatting and detector-registry helpers for ``python -m benchmark`` and -``python -m benchmark.sweep``. - -Kept intentionally small: the two entry points have different prediction loops (one -default-kwargs pass vs a fan-out parameter sweep) but render results into the same -tables. -""" - -from __future__ import annotations - -import json -import math -from typing import Any - -from benchmark.evaluator import BenchmarkResult -from scenedetect import ( - AdaptiveDetector, - ContentDetector, - HashDetector, - HistogramDetector, - ThresholdDetector, -) - -DEFAULT_BACKEND = "opencv" - -DETECTORS: dict[str, type] = { - "detect-adaptive": AdaptiveDetector, - "detect-content": ContentDetector, - "detect-hash": HashDetector, - "detect-hist": HistogramDetector, - "detect-threshold": ThresholdDetector, -} - - -def parse_tolerances(spec: str) -> tuple[int, ...]: - """Parse ``"0,1,5"`` into ``(0, 1, 5)``. Blank entries (e.g. trailing comma) are dropped.""" - return tuple(int(x.strip()) for x in spec.split(",") if x.strip()) - - -def fmt_pct(value: float, count: int) -> str: - """Percentage, or ``n/a`` when the underlying class has zero events.""" - return "n/a" if count == 0 else f"{value * 100:.2f}" - - -def fmt_offset(value: float) -> str: - return "n/a" if math.isnan(value) else f"{value:.3f}" - - -def render_table(header: list[str], rows: list[list[str]]) -> str: - """Build a pipe-delimited GitHub-flavored Markdown table as a single string.""" - widths = [max(len(header[i]), *(len(r[i]) for r in rows)) for i in range(len(header))] - sep = "| " + " | ".join("-" * w for w in widths) + " |" - header_line = "| " + " | ".join(h.ljust(w) for h, w in zip(header, widths, strict=True)) + " |" - body = [ - "| " + " | ".join(c.ljust(w) for c, w in zip(r, widths, strict=True)) + " |" for r in rows - ] - return "\n".join([header_line, sep, *body]) - - -HARD_HEADER = ["Tolerance", "Precision", "Recall", "F1", "Offset", "Elapsed"] -FADE_HEADER = ["Tolerance", "Precision", "Recall", "F1"] - - -def hard_row(result: BenchmarkResult) -> list[str]: - hard = result.hard_cuts - hard_predictions = hard.matched + hard.false_positives - hard_events = hard.matched + hard.missed - return [ - str(result.tolerance), - fmt_pct(hard.precision, hard_predictions), - fmt_pct(hard.recall, hard_events), - fmt_pct(hard.f1, hard_events), - fmt_offset(result.mean_abs_offset_hard_cuts), - f"{result.elapsed_mean:.2f}", - ] - - -def fade_row(result: BenchmarkResult) -> list[str]: - fades = result.fades - fade_predictions = fades.matched + fades.false_positives - fade_events = fades.matched + fades.missed - return [ - str(result.tolerance), - fmt_pct(fades.precision, fade_predictions), - fmt_pct(fades.recall, fade_events), - fmt_pct(fades.f1, fade_events), - ] - - -def write_json(out_path: str, payload: dict[str, Any]) -> None: - with open(out_path, "w") as f: - json.dump(payload, f, indent=2, default=str) - print(f"\nWrote results to {out_path}") diff --git a/benchmark/analyze_sweep.py b/benchmark/analyze_sweep.py deleted file mode 100644 index 01db94f3..00000000 --- a/benchmark/analyze_sweep.py +++ /dev/null @@ -1,272 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# -"""Derive default-parameter recommendations from sweep results. - -Applies a fixed decision procedure to the grid JSONs under ``benchmark/results/sweep/`` -rather than just taking the argmax of mean F1@1: - -1. Baseline the shipped default (nearest grid cell). -2. Candidate set: cells within ``EPSILON`` of the best mean F1@1 (the plateau, not the peak). -3. Neighborhood robustness: reject cells with a steep drop to any one-grid-step neighbor - along a fine-grained numeric axis (categorical axes like ``size``/``bins`` are not - "steps" and are excluded). -4. Weighting sensitivity: a candidate must beat the default under the equal-dataset mean - and under every leave-one-dataset-out mean -- i.e. the *improvement* survives removing - any single dataset. (Being within EPSILON of each scheme's argmax is hopeless when - per-dataset optima diverge, and is not the question a defaults change asks.) The pooled - micro-average is reported for context but is not a gate: pooling events lets the - largest corpus (ClipShots, ~10x the cuts) dominate, making it a dataset-weighting - choice rather than a robustness check. -5. Precision floor: per dataset, candidate precision@1 must not fall more than - ``PRECISION_SLACK`` below the default's. -6. Materiality: recommend a change only for >= ``MIN_GAIN`` mean F1@1 over the default, - gains >= 1.0 on at least two datasets, and no dataset regressing by > 1.0. -7. min_scene_len isolation: for detectors that swept it, also rank with it fixed at the - default-equivalent slice so the threshold recommendation stands on its own. - -Prints a markdown report to stdout. Requires the sweep JSONs locally (not committed); -see ``scripts/benchmark_sweep.sh``. -""" - -from __future__ import annotations - -from benchmark.report_sweep import DATASETS, _load_cells, _params_str, _table - -EPSILON = 1.0 # candidate set: within this many F1 points of the best mean -MAX_NEIGHBOR_DROP = 2.0 # reject cells this much better than their worst neighbor -FINE_AXIS_MIN_VALUES = 4 # axes with fewer distinct values are categorical, not grid steps -PRECISION_SLACK = 5.0 # per-dataset precision@1 may not drop more than this vs default -MIN_GAIN = 2.0 # mean F1@1 gain required to recommend changing a default - -# Shipped defaults mapped onto the swept grid (nearest cell). min_scene_len defaults to -# 15 *frames*; the sweeps used seconds, so 0.6 matches only at 25 fps (BBC) and is ~0.5 -# at 30 fps web video -- flagged in the report. hash's 0.395 maps to the 0.4 grid point. -DEFAULTS: dict[str, dict] = { - "detect-content": {"min_scene_len": 0.6, "threshold": 27}, - "detect-adaptive": {"adaptive_threshold": 3.0, "min_scene_len": 0.6, "window_width": 2}, - "detect-hash": {"size": 16, "threshold": 0.4}, - "detect-hist": {"bins": 256, "threshold": 0.05}, -} -MSL_SWEPT = {"detect-content", "detect-adaptive"} - - -def _f1(matched: int, fp: int, missed: int) -> float: - p = matched / (matched + fp) if matched + fp else 0.0 - r = matched / (matched + missed) if matched + missed else 0.0 - return 200.0 * p * r / (p + r) if p + r else 0.0 - - -class Cell: - """One parameter combination with per-dataset hard-cut results at tolerance 1.""" - - def __init__(self, params: dict, per_ds: dict[str, dict]): - self.params = params - self.key = _params_str(params) - self.per_ds = per_ds # dataset -> hard_cuts dict (matched/fp/missed/precision/recall/f1) - self.mean_f1 = sum(d["f1"] for d in per_ds.values()) / len(per_ds) - self.micro_f1 = _f1( - sum(d["matched"] for d in per_ds.values()), - sum(d["false_positives"] for d in per_ds.values()), - sum(d["missed"] for d in per_ds.values()), - ) - - def lodo(self, skip: str) -> float: - rest = [d["f1"] for ds, d in self.per_ds.items() if ds != skip] - return sum(rest) / len(rest) - - -def _load(det: str) -> list[Cell]: - per_key: dict[str, dict[str, dict]] = {} - params_by_key: dict[str, dict] = {} - for ds in DATASETS: - cells = _load_cells(det, ds) - if cells is None: - return [] - for c in cells: - key = _params_str(c["params"]) - per_key.setdefault(key, {})[ds] = c["results"]["1"]["aggregate"]["hard_cuts"] - params_by_key[key] = c["params"] - return [Cell(params_by_key[k], v) for k, v in per_key.items() if len(v) == len(DATASETS)] - - -def _neighbors(cell: Cell, cells: list[Cell]) -> list[Cell]: - """Cells one grid step away along exactly one fine-grained numeric axis. - - Axes with fewer than ``FINE_AXIS_MIN_VALUES`` distinct values (e.g. ``size=8,16``, - ``bins=128,256``, ``window_width=2,3``) are categorical choices, not grid steps, so - a large score difference across them is not a knife-edge. - """ - axes = {k: sorted({c.params[k] for c in cells}) for k in cell.params} - out = [] - for other in cells: - diff = [k for k in cell.params if other.params[k] != cell.params[k]] - if len(diff) != 1: - continue - (k,) = diff - vals = axes[k] - if len(vals) < FINE_AXIS_MIN_VALUES: - continue - if abs(vals.index(other.params[k]) - vals.index(cell.params[k])) == 1: - out.append(other) - return out - - -def analyze(det: str) -> list[str]: - cells = _load(det) - out = [f"## {det}\n"] - if not cells: - return [*out, "(sweep JSONs missing)\n"] - - # Match by the canonical params string: grid generation leaves float artifacts - # (e.g. 0.4000000000000001) that a plain dict comparison would miss. - default_key = _params_str(DEFAULTS[det]) - default = next((c for c in cells if c.key == default_key), None) - best = max(cells, key=lambda c: c.mean_f1) - - def row(c: Cell, label: str) -> list[str]: - return [ - label, - f"{c.mean_f1:.2f}", - *(f"{c.per_ds[ds]['f1']:.2f}" for ds in DATASETS), - c.key, - ] - - rows = [row(best, "best")] - if default is not None: - rows.insert(0, row(default, "default")) - out.append(_table(["Cell", "Mean F1@1", *DATASETS, "Params"], rows)) - out.append("") - if default is None: - out.append(f"> Default cell {DEFAULTS[det]} not present in the grid; criteria that") - out.append("> compare against the default are skipped below.\n") - - # Gated weighting schemes: equal-dataset mean + leave-one-dataset-out means. A - # candidate is weighting-stable if it beats the default under every scheme, i.e. the - # improvement does not hinge on any single dataset. Micro-average is reported per - # candidate but deliberately not gated (see module docstring). Without a default cell - # to compare against, fall back to within-EPSILON-of-best per scheme. - schemes = [lambda c: c.mean_f1] - schemes += [lambda c, ds=ds: c.lodo(ds) for ds in DATASETS] - scheme_floor = ( - [s(default) for s in schemes] - if default is not None - else [max(s(c) for c in cells) - EPSILON for s in schemes] - ) - - candidates = sorted( - (c for c in cells if c.mean_f1 >= best.mean_f1 - EPSILON), - key=lambda c: c.mean_f1, - reverse=True, - ) - cand_rows = [] - passing = [] - for c in candidates: - nbrs = _neighbors(c, cells) - worst_drop = max((c.mean_f1 - n.mean_f1 for n in nbrs), default=0.0) - robust = worst_drop <= MAX_NEIGHBOR_DROP - stable = all(s(c) >= floor for s, floor in zip(schemes, scheme_floor, strict=True)) - if default is not None: - prec_ok = all( - c.per_ds[ds]["precision"] >= default.per_ds[ds]["precision"] - PRECISION_SLACK - for ds in DATASETS - ) - deltas = [c.per_ds[ds]["f1"] - default.per_ds[ds]["f1"] for ds in DATASETS] - material = ( - c.mean_f1 - default.mean_f1 >= MIN_GAIN - and sum(d >= 1.0 for d in deltas) >= 2 - and all(d >= -1.0 for d in deltas) - ) - else: - prec_ok = material = True - ok = robust and stable and prec_ok and material - if ok: - passing.append(c) - mark = lambda b: "yes" if b else "NO" # noqa: E731 - cand_rows.append( - [ - f"{c.mean_f1:.2f}", - f"{c.micro_f1:.2f}", - f"{worst_drop:.2f}", - mark(robust), - mark(stable), - mark(prec_ok), - mark(material), - "PASS" if ok else "-", - c.key, - ] - ) - out.append(f"**Candidates (mean F1@1 within {EPSILON:g} of best):**\n") - out.append( - _table( - [ - "Mean", - "Micro", - "NbrDrop", - "Robust", - "WeightStable", - "PrecFloor", - "Material", - "Verdict", - "Params", - ], - cand_rows, - ) - ) - out.append("") - - if det in MSL_SWEPT: - msl = DEFAULTS[det]["min_scene_len"] - fixed = [c for c in cells if c.params.get("min_scene_len") == msl] - top = sorted(fixed, key=lambda c: c.mean_f1, reverse=True)[:3] - out.append(f"**With min_scene_len fixed at the default-equivalent {msl:g}s:**\n") - out.append( - _table( - ["Mean F1@1", *DATASETS, "Params"], - [ - [f"{c.mean_f1:.2f}", *(f"{c.per_ds[ds]['f1']:.2f}" for ds in DATASETS), c.key] - for c in top - ], - ) - ) - out.append("") - - if passing: - pick = passing[0] - gain = f" (+{pick.mean_f1 - default.mean_f1:.2f} mean F1@1 vs default)" if default else "" - out.append(f"**Recommendation: CHANGE to `{pick.key}`{gain}.**\n") - else: - out.append( - "**Recommendation: KEEP current default** (no candidate passes all criteria; " - "see verdict column for which gate fails).\n" - ) - return out - - -def main() -> None: - print("# Detector default recommendations from sweep data\n") - print( - "Generated by `benchmark/analyze_sweep.py`. Criteria: candidate plateau within " - f"{EPSILON:g} F1 of best; worst one-step neighbor drop <= {MAX_NEIGHBOR_DROP:g} along " - "fine-grained axes; beats the default under equal-mean and each leave-one-dataset-out " - "weighting (pooled micro-average reported, not gated); " - f"per-dataset precision floor (default - " - f"{PRECISION_SLACK:g}); materiality (>= {MIN_GAIN:g} mean F1@1 gain, >= 1.0 on two " - "datasets, no dataset worse by > 1.0). min_scene_len defaults to 15 frames (= 0.6s " - "only at 25 fps); the default cell uses the nearest swept slice.\n" - ) - for det in DEFAULTS: - for line in analyze(det): - print(line) - - -if __name__ == "__main__": - main() diff --git a/benchmark/dataset.py b/benchmark/dataset.py deleted file mode 100644 index e0a73a91..00000000 --- a/benchmark/dataset.py +++ /dev/null @@ -1,240 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# -"""Benchmark dataset definitions and registry. - -Each :class:`Dataset` is a corpus of :class:`Sample` records (video file + typed ground truth) -loaded eagerly at construction. Ground-truth files for the supported corpora are at most a few -hundred kilobytes total, so eager loading avoids re-reading the same files for every sweep cell. - -Add a new dataset by: - -1. Subclassing :class:`Dataset` and populating ``self._samples`` in ``__init__``. -2. Registering it in :data:`DATASETS` under the name used by ``--dataset``. -""" - -from __future__ import annotations - -import glob -import json -import logging -import os -from collections.abc import Iterator -from dataclasses import dataclass -from pathlib import Path - -from benchmark.evaluator import EventInterval, Frames, GroundTruth - -logger = logging.getLogger("pyscenedetect") - - -@dataclass(frozen=True) -class Sample: - """One scored video: a path on disk plus its typed ground truth.""" - - video_file: Path - ground_truth: GroundTruth - - -class Dataset: - """Iterable corpus of :class:`Sample` records. - - Subclasses populate ``self._samples`` in their constructor; this base provides the iteration - and length protocol. ``event_types`` advertises which TRECVID-SBD event categories the - dataset's ground truth contains, so consumers can skip columns/tables for categories that - have no events (e.g. fade transitions on BBC/AutoShot). - """ - - event_types: frozenset[str] = frozenset({"hard_cut"}) - _samples: list[Sample] - - def __iter__(self) -> Iterator[Sample]: - return iter(self._samples) - - def __len__(self) -> int: - return len(self._samples) - - -def _read_tab_separated_cuts(scene_file: str) -> list[Frames]: - """Parse a BBC/AutoShot-style annotation file. - - Each line is tab-separated; the second column is the 0-based frame index of a - hard cut. Returns 1-based frame indices, matching the convention used by - :class:`scenedetect.FrameTimecode`. - """ - with open(scene_file) as f: - return [int(line.strip().split("\t")[1]) + 1 for line in f] - - -class BBCDataset(Dataset): - """The BBC Planet Earth dataset. - - Baraldi et al., "A Deep Siamese Network for Scene Detection in Broadcast Videos", - ACM Multimedia 2015. https://arxiv.org/abs/1510.08893 - - 11 long-form videos (``BBC/videos/bbc_.mp4``) with hard-cut annotations in - ``BBC/fixed/-scenes.txt``. - """ - - def __init__(self, dataset_dir: str): - video_files = sorted(glob.glob(os.path.join(dataset_dir, "videos", "*.mp4"))) - scene_files = sorted(glob.glob(os.path.join(dataset_dir, "fixed", "*.txt"))) - if len(video_files) != len(scene_files): - raise ValueError( - f"BBC dataset at {dataset_dir!r}: {len(video_files)} videos but " - f"{len(scene_files)} annotation files." - ) - self._samples: list[Sample] = [] - for video_file, scene_file in zip(video_files, scene_files, strict=True): - video_id = os.path.basename(video_file).replace("bbc_", "").split(".")[0] - scene_id = os.path.basename(scene_file).split("-")[0] - if video_id != scene_id: - raise ValueError(f"BBC id mismatch: {video_file} vs {scene_file}") - self._samples.append( - Sample( - video_file=Path(video_file), - ground_truth=GroundTruth(hard_cuts=_read_tab_separated_cuts(scene_file)), - ) - ) - - -class AutoShotDataset(Dataset): - """The AutoShot dataset (test splits). - - Zhu et al., "AutoShot: A Short Video Dataset and State-of-the-Art Shot Boundary - Detection", CVPRW 2023. The original test set has 200 videos; 36 are no longer - publicly available, so the corpus iterates over whatever is present on disk. - - Videos at ``AutoShot/videos/.mp4``, hard-cut annotations at - ``AutoShot/annotations/.txt``. - """ - - def __init__(self, dataset_dir: str): - # 36 of the original 200 videos are no longer publicly available, so intersect - # by id rather than zipping the directory listings strictly. - videos_by_id = { - os.path.basename(p).split(".")[0]: p - for p in glob.glob(os.path.join(dataset_dir, "videos", "*.mp4")) - } - scenes_by_id = { - os.path.basename(p).split(".")[0]: p - for p in glob.glob(os.path.join(dataset_dir, "annotations", "*.txt")) - } - self._samples: list[Sample] = [ - Sample( - video_file=Path(videos_by_id[vid]), - ground_truth=GroundTruth(hard_cuts=_read_tab_separated_cuts(scenes_by_id[vid])), - ) - for vid in sorted(videos_by_id.keys() & scenes_by_id.keys()) - ] - - -class ClipShotsDataset(Dataset): - """The ClipShots dataset (test split by default). - - Tang et al., "Fast Video Shot Transition Localization with Deep Structured Models", - ACCV 2018. https://github.com/Tangshitao/ClipShots - - The only in-tree dataset with typed gradual-transition (fade/dissolve) ground truth in - addition to hard cuts. Layout under ``ClipShots/``:: - - annotations/{train,test,only_gradual}.json - video_lists/{train,test,only_gradual}.txt (optional split filter) - videos/*.mp4 - - Each annotation entry is ``{"transitions": [[start, end], ...], "frame_num": float}``. - Hard cuts are single-frame spans (``end == start + 1``); wider spans are gradual - transitions. Unlike the BBC/AutoShot annotations, ClipShots frame indices already match - PySceneDetect's boundary-frame convention (the prediction's ``frame_num`` lines up with - ``transition[1]`` directly), so no offset is applied here. - - Loading rules: - - - Videos listed in ``video_lists/.txt`` but absent from the annotations JSON are - silently ignored (the filter runs against the JSON, not the other way). - - Annotations whose ``.mp4`` is not on disk are skipped (so partial corpora work). - - Malformed transitions (fewer than 2 entries, negative span, zero-width span) are - skipped with a warning rather than crashing the load. - - Only the ``ClipShotsDataset(dir, split=...)`` constructor honors a non-default split; - the registry entry in :data:`DATASETS` always loads the ``test`` split. - """ - - event_types = frozenset({"hard_cut", "fade"}) - - def __init__(self, dataset_dir: str, split: str = "test"): - ann_path = os.path.join(dataset_dir, "annotations", f"{split}.json") - videos_dir = os.path.join(dataset_dir, "videos") - with open(ann_path) as f: - annotations: dict = json.load(f) - split_list_path = os.path.join(dataset_dir, "video_lists", f"{split}.txt") - if os.path.exists(split_list_path): - with open(split_list_path) as allow_f: - allowed = {line.strip() for line in allow_f if line.strip()} - annotations = {k: v for k, v in annotations.items() if k in allowed} - total = len(annotations) - skipped_missing = 0 - self._samples: list[Sample] = [] - for video_name in sorted(annotations): - video_path = os.path.join(videos_dir, video_name) - if not os.path.exists(video_path): - skipped_missing += 1 - continue - hard_cuts: list[Frames] = [] - fades: list[EventInterval] = [] - # `... or []` (not `.get(k, [])`) so an explicit JSON `null` is treated as empty. - for transition in annotations[video_name].get("transitions") or []: - if len(transition) < 2: - logger.warning("ClipShots %s: malformed transition %r", video_name, transition) - continue - start, end = int(transition[0]), int(transition[1]) - span = end - start - if span == 1: - hard_cuts.append(end) - elif span > 1: - fades.append(EventInterval(start=start, end=end)) - else: - logger.warning( - "ClipShots %s: skipping degenerate transition %r", video_name, transition - ) - self._samples.append( - Sample( - video_file=Path(video_path), - ground_truth=GroundTruth(hard_cuts=hard_cuts, fades=fades), - ) - ) - logger.info( - "ClipShots %s: loaded %d/%d samples (%d videos missing on disk)", - split, - len(self._samples), - total, - skipped_missing, - ) - - -# Mapping of --dataset names to constructors. Typed as a plain callable so -# subclass-specific positional signatures (each takes ``dataset_dir: str``) -# aren't widened away by the base ``Dataset`` class's empty ``__init__``. -DATASETS: dict[str, type] = { - "BBC": BBCDataset, - "AutoShot": AutoShotDataset, - "ClipShots": ClipShotsDataset, -} - - -def resolve_dataset(name: str, root: str | None) -> Dataset: - """Instantiate the named dataset. - - ``root`` overrides the default repo-relative path; pass ``None`` (or the empty string) - to use ``benchmark//``. - """ - base = root if root else "benchmark" - return DATASETS[name](os.path.join(base, name)) diff --git a/benchmark/evaluator.py b/benchmark/evaluator.py deleted file mode 100644 index 07a666b6..00000000 --- a/benchmark/evaluator.py +++ /dev/null @@ -1,346 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# -"""Scoring for shot-boundary-detection benchmarks. - -Implements the TRECVID-SBD evaluation convention. Each predicted boundary is one integer frame -number. Hard cuts are matched against ground-truth frames, with a configurable frame-tolerance. -Matches are scored via greedy 1-to-1 nearest-neighbor assignment. Fades and other gradual -transitions are matched by point-in-interval membership, where the prediction inside an interval -is considered a match. Other predictions in the same interval are considered false positives. - -References: -- Smeaton, Over & Doherty (2010), "Video shot boundary detection: Seven years of TRECVid activity", - *Computer Vision and Image Understanding*. - https://ora.ox.ac.uk/objects/uuid:868aebdf-298a-4567-b47f-c8f9e3a6ac7a -- Hassanien et al. (2017), "Large-scale, Fast and Accurate Shot Boundary Detection through - Spatio-temporal Convolutional Neural Networks", arXiv:1705.03281. - https://arxiv.org/abs/1705.03281 -""" - -from __future__ import annotations - -import math -from collections.abc import Iterable -from dataclasses import dataclass, field -from pathlib import Path -from statistics import mean -from typing import TypeAlias - -# 1-based frame number, matching the convention used by the BBC/AutoShot text annotations and by -# PySceneDetect's :class:`FrameTimecode`. Used for cut positions and for tolerance windows. -# -# Ironically, all the work we did in v0.7 to support VFR is meaningless for most existing benchmarks -# since they are all CFR. In the future we should consider extending the API to support temporal -# units of time or PTS, and also see if other datasets might take this into account. -Frames: TypeAlias = int - - -@dataclass(frozen=True) -class EventInterval: - """Inclusive ``[start, end]`` frame range for a gradual transition (dissolve/fade).""" - - start: Frames - end: Frames - - def contains(self, frame: Frames) -> bool: - return self.start <= frame <= self.end - - -@dataclass -class GroundTruth: - """Ground truth for one video, consisting of hard cut frames and fade intervals.""" - - hard_cuts: list[Frames] - fades: list[EventInterval] = field(default_factory=list) - category: str | None = None - - -@dataclass -class Prediction: - """One detector run on one video, ready for scoring against typed ground truth.""" - - predicted_cuts: list[Frames] - """Flat list of predicted hard cut frame numbers, 1-based.""" - ground_truth: GroundTruth - """Ground truth for the video being scored.""" - elapsed: float - """How long it took to run the prediction, in seconds. Used for performance not accuracy.""" - - -@dataclass -class EventMetrics: - """Per-event-type scoring counts used to calculate precision, recall, and F1 score. - - Each instance should be used to score *one* event type (either hard cuts *or* fade transitions) - against ground truth. - """ - - # Detector fired on a real event in the ground truth. - matched: int = 0 - # Detector fired but there was no real event at that frame. - false_positives: int = 0 - # Real event in the ground truth that the detector failed to fire on. - missed: int = 0 - - @property - def precision(self) -> float: - denom = self.matched + self.false_positives - return self.matched / denom if denom else 0.0 - - @property - def recall(self) -> float: - denom = self.matched + self.missed - return self.matched / denom if denom else 0.0 - - @property - def f1(self) -> float: - p, r = self.precision, self.recall - return 2 * p * r / (p + r) if (p + r) else 0.0 - - def __add__(self, other: EventMetrics) -> EventMetrics: - return EventMetrics( - matched=self.matched + other.matched, - false_positives=self.false_positives + other.false_positives, - missed=self.missed + other.missed, - ) - - def to_dict(self) -> dict: - return { - "matched": self.matched, - "false_positives": self.false_positives, - "missed": self.missed, - "precision": round(self.precision * 100, 4), - "recall": round(self.recall * 100, 4), - "f1": round(self.f1 * 100, 4), - } - - -@dataclass -class VideoMetrics: - """Per-video result at one tolerance. The video's path lives in the enclosing - :class:`BenchmarkResult.per_video` dict key, not on this object.""" - - elapsed: float - category: str | None - hard_cuts: EventMetrics - fades: EventMetrics - # (sum of |prediction - ground_truth|, match count) over hard-cut matches. - # Stored as raw sums so aggregation across videos is `sum / total_matched`, - # not a mean-of-means. - hard_offset: tuple[float, int] - - @property - def mean_abs_offset(self) -> float: - s, n = self.hard_offset - return s / n if n else math.nan - - def to_dict(self) -> dict: - return { - "elapsed": self.elapsed, - "category": self.category, - "hard_cuts": self.hard_cuts.to_dict(), - "fades": self.fades.to_dict(), - "mean_abs_offset_hard_cuts": self.mean_abs_offset, - } - - -@dataclass -class BenchmarkResult: - """Aggregate result of running one detector configuration on a dataset at one tolerance. - - ``per_video`` is keyed by source video path so per-video lookups are explicit; aggregate - properties sum counts across all videos (same convention used by TRECVID). - """ - - per_video: dict[Path, VideoMetrics] - tolerance: Frames - - @property - def hard_cuts(self) -> EventMetrics: - total = EventMetrics() - for v in self.per_video.values(): - total = total + v.hard_cuts - return total - - @property - def fades(self) -> EventMetrics: - total = EventMetrics() - for v in self.per_video.values(): - total = total + v.fades - return total - - @property - def mean_abs_offset_hard_cuts(self) -> float: - num = sum(v.hard_offset[0] for v in self.per_video.values()) - den = sum(v.hard_offset[1] for v in self.per_video.values()) - return num / den if den else math.nan - - @property - def elapsed_total(self) -> float: - return sum(v.elapsed for v in self.per_video.values()) - - @property - def elapsed_mean(self) -> float: - return mean(v.elapsed for v in self.per_video.values()) if self.per_video else 0.0 - - def by_category(self) -> dict[str, BenchmarkResult]: - buckets: dict[str, dict[Path, VideoMetrics]] = {} - for path, v in self.per_video.items(): - buckets.setdefault(v.category or "unknown", {})[path] = v - return { - g: BenchmarkResult(per_video=vids, tolerance=self.tolerance) - for g, vids in buckets.items() - } - - def to_dict(self, root: Path | None = None) -> dict: - def _fmt_path(p: Path) -> str: - if root is not None: - try: - return p.relative_to(root).as_posix() - except ValueError: - pass - return p.as_posix() - - return { - "tolerance": self.tolerance, - "aggregate": { - "hard_cuts": self.hard_cuts.to_dict(), - "mean_abs_offset_hard_cuts": self.mean_abs_offset_hard_cuts, - "fades": self.fades.to_dict(), - "elapsed_total": self.elapsed_total, - "elapsed_mean": self.elapsed_mean, - "video_count": len(self.per_video), - }, - "per_video": {_fmt_path(path): v.to_dict() for path, v in self.per_video.items()}, - } - - -def _score_hard_cuts( - predicted_cuts: Iterable[Frames], - ground_truth_cuts: Iterable[Frames], - tolerance: Frames, -) -> tuple[EventMetrics, list[Frames]]: - """Greedy 1-to-1 nearest-neighbor matching within ``tolerance`` frames. - - Builds the set of all (prediction, ground-truth) candidate pairs whose absolute frame distance - is within tolerance, sorts by distance, and walks the sorted list claiming the first unused - pair each time. Ties on distance are broken by stable iteration order, which is deterministic - but otherwise unspecified - fine since we report aggregate metrics, not per-event assignments. - - Returns the event metrics and the per-match absolute offsets (for later averaging). - """ - predicted_cuts = list(predicted_cuts) - ground_truth_cuts = list(ground_truth_cuts) - candidates: list[tuple[int, int, int]] = [] - for i, p in enumerate(predicted_cuts): - for j, g in enumerate(ground_truth_cuts): - d = abs(p - g) - if d <= tolerance: - candidates.append((d, i, j)) - candidates.sort() - prediction_used = [False] * len(predicted_cuts) - ground_truth_used = [False] * len(ground_truth_cuts) - offsets: list[int] = [] - for d, i, j in candidates: - if not prediction_used[i] and not ground_truth_used[j]: - prediction_used[i] = True - ground_truth_used[j] = True - offsets.append(d) - matched = len(offsets) - return ( - EventMetrics( - matched=matched, - false_positives=len(predicted_cuts) - matched, - missed=len(ground_truth_cuts) - matched, - ), - offsets, - ) - - -def _score_fade_transitions( - predicted_cuts: Iterable[Frames], - intervals: Iterable[EventInterval], -) -> tuple[EventMetrics, set[int]]: - """Point-in-interval matching for gradual fade transitions. - - Each prediction that falls inside any ground-truth interval is consumed by that interval - (first-match wins). The first prediction to land in an interval is the match; any further - predictions in the same interval are false positives. Predictions outside every interval are - not touched here - they go back to the hard-cut scorer. - - Returns the fade transition metrics and the set of *positional indices* (into - ``predicted_cuts``, not frame values) that were consumed by a fade interval, so the caller - can skip them when running hard matching. - """ - predicted_cuts = list(predicted_cuts) - intervals = list(intervals) - consumed: set[int] = set() - intervals_matched: set[EventInterval] = set() - matched = 0 - false_positives = 0 - for k, p in enumerate(predicted_cuts): - for interval in intervals: - if interval.contains(p): - consumed.add(k) - if interval in intervals_matched: - false_positives += 1 - else: - intervals_matched.add(interval) - matched += 1 - break - missed = len(intervals) - matched - return ( - EventMetrics(matched=matched, false_positives=false_positives, missed=missed), - consumed, - ) - - -def score_video( - predicted_cuts: Iterable[Frames], - ground_truth: GroundTruth, - tolerance: Frames, - elapsed: float, -) -> VideoMetrics: - """Score one video against typed ground truth at one tolerance. - - Fade transition matching runs first; predictions that land inside any fade interval are - consumed there and excluded from hard-cut matching. The remaining predictions are matched - against ground-truth hard cuts at ``tolerance`` frames. - """ - predicted_cuts = list(predicted_cuts) - - fade_metrics, consumed = _score_fade_transitions(predicted_cuts, ground_truth.fades) - remaining_cuts = [p for k, p in enumerate(predicted_cuts) if k not in consumed] - hard_metrics, offsets = _score_hard_cuts(remaining_cuts, ground_truth.hard_cuts, tolerance) - - return VideoMetrics( - elapsed=elapsed, - category=ground_truth.category, - hard_cuts=hard_metrics, - fades=fade_metrics, - hard_offset=(float(sum(offsets)), len(offsets)), - ) - - -def evaluate(predictions: dict[Path, Prediction], tolerance: Frames) -> BenchmarkResult: - """Score predictions at a single tolerance and return aggregate + per-video results.""" - assert predictions, "predictions must not be empty" - videos = { - path: score_video( - predicted_cuts=p.predicted_cuts, - ground_truth=p.ground_truth, - tolerance=tolerance, - elapsed=p.elapsed, - ) - for path, p in predictions.items() - } - return BenchmarkResult(per_video=videos, tolerance=tolerance) diff --git a/benchmark/report_sweep.py b/benchmark/report_sweep.py deleted file mode 100644 index 854049fa..00000000 --- a/benchmark/report_sweep.py +++ /dev/null @@ -1,149 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# -"""Consolidate per-(detector, dataset) sweep JSONs into a single markdown report. - -Reads ``benchmark/results/sweep/-.json`` for all five detectors and writes -``benchmark/SWEEP_REPORT.md``: the best cell by F1@1 per (detector, dataset), the top-5 -per cell, and the cell that is best *on average* across datasets per detector (a single -recommended default). - -All results come from the decode-based sweep (``benchmark/sweep.py``, driven by -``scripts/benchmark_sweep.sh``). Unlike content/adaptive, hash/hist were swept with the default -``min_scene_len`` fixed. If a ``-ext-.json`` grid-extension file exists -alongside the main JSON, its cells are merged in. -""" - -from __future__ import annotations - -import json -from pathlib import Path - -DETECTORS = [ - "detect-content", - "detect-adaptive", - "detect-hash", - "detect-hist", - "detect-threshold", -] -DATASETS = ["BBC", "AutoShot", "ClipShots"] -SWEEP_DIR = Path("benchmark/results/sweep") -OUT = Path("benchmark/SWEEP_REPORT.md") - - -def _hard(cell: dict, tol: str) -> dict: - return cell["results"][tol]["aggregate"]["hard_cuts"] - - -def _params_str(p: dict) -> str: - # :g strips float artifacts from grid generation (e.g. 0.42500000000000016 -> 0.425). - def fmt(v): - return f"{v:g}" if isinstance(v, float) else str(v) - - return ", ".join(f"{k}={fmt(v)}" for k, v in sorted(p.items())) - - -def _table(header: list[str], rows: list[list[str]]) -> str: - widths = [max(len(header[i]), *(len(r[i]) for r in rows)) for i in range(len(header))] - - def line(cells): - return "| " + " | ".join(c.ljust(w) for c, w in zip(cells, widths, strict=True)) + " |" - - sep = "| " + " | ".join("-" * w for w in widths) + " |" - return "\n".join([line(header), sep, *(line(r) for r in rows)]) - - -def _load_cells(det: str, ds: str) -> list[dict] | None: - path = SWEEP_DIR / f"{det}-{ds}.json" - if not path.exists(): - return None - cells = json.loads(path.read_text())["cells"] - ext = SWEEP_DIR / f"{det}-ext-{ds}.json" - if ext.exists(): - cells = cells + json.loads(ext.read_text())["cells"] - return cells - - -def main() -> None: - out: list[str] = ["# Detector parameter sweep report", ""] - out.append( - "Generated by `benchmark/report_sweep.py` from `benchmark/sweep.py` grid results " - "(hash/hist were swept with `min_scene_len` fixed at its default). " - "F1/precision/recall are percentages on hard cuts; tol = frame tolerance.\n" - ) - - for det in DETECTORS: - out.append(f"## {det}\n") - if det == "detect-threshold": - out.append( - "> Note: `detect-threshold` detects **fades** (fade to/from black), not hard " - "cuts. These datasets' ground truth is hard cuts, so the hard-cut F1 below is " - "expectedly near zero. It is included to validate the sweep pipeline end-to-end, " - "not as a meaningful hard-cut accuracy result.\n" - ) - if det == "detect-hist": - out.append( - "> Note: thresholds >= 0.21 come from a grid-extension run " - "(`detect-hist-ext-.json`) after the initial grid's best cell landed " - "on its 0.20 upper edge.\n" - ) - # Best-per-dataset summary. - summary_rows = [] - # Track each cell's F1@1 across datasets for an averaged recommendation. - per_cell_f1: dict[str, list[float]] = {} - per_cell_params: dict[str, dict] = {} - for ds in DATASETS: - cells = _load_cells(det, ds) - if cells is None: - summary_rows.append([ds, "(missing)", "", "", "", ""]) - continue - best = max(cells, key=lambda c: _hard(c, "1")["f1"]) - h1, h0 = _hard(best, "1"), _hard(best, "0") - summary_rows.append( - [ - ds, - f"{h1['f1']:.2f}", - f"{h1['precision']:.2f}", - f"{h1['recall']:.2f}", - f"{h0['f1']:.2f}", - _params_str(best["params"]), - ] - ) - for c in cells: - key = _params_str(c["params"]) - per_cell_f1.setdefault(key, []).append(_hard(c, "1")["f1"]) - per_cell_params[key] = c["params"] - out.append("**Best cell per dataset (by F1@1):**\n") - out.append( - _table( - ["Dataset", "F1@1", "Prec@1", "Rec@1", "F1@0", "Params"], - summary_rows, - ) - ) - out.append("") - # Averaged recommendation: cells scored on all datasets, ranked by mean F1@1. - full = {k: v for k, v in per_cell_f1.items() if len(v) == len(DATASETS)} - if full: - ranked = sorted(full.items(), key=lambda kv: sum(kv[1]) / len(kv[1]), reverse=True) - rec_rows = [ - [f"{sum(v) / len(v):.2f}", *(f"{x:.2f}" for x in v), k] for k, v in ranked[:5] - ] - out.append("**Best params averaged across all datasets (mean F1@1):**\n") - out.append(_table(["Mean F1@1", *DATASETS, "Params"], rec_rows)) - out.append("") - - OUT.write_text("\n".join(out), encoding="utf-8") - print(f"Wrote {OUT}") - print("\n".join(out)) - - -if __name__ == "__main__": - main() diff --git a/benchmark/sweep.py b/benchmark/sweep.py deleted file mode 100644 index dbd20d78..00000000 --- a/benchmark/sweep.py +++ /dev/null @@ -1,452 +0,0 @@ -# -# PySceneDetect: Python-Based Video Scene Detector -# ------------------------------------------------------------------- -# [ Site: https://scenedetect.com ] -# [ Docs: https://scenedetect.com/docs/ ] -# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] -# -# Copyright (C) 2026 Brandon Castellano . -# PySceneDetect is licensed under the BSD 3-Clause License; see the -# included LICENSE file, or visit one of the above pages for details. -# -"""Parameter sweep harness for one detector on one dataset. - -Brute-force grid search over a Cartesian product of detector parameters. The cost of -the grid is amortized using :class:`FanOutVideoStream`. One video decode per chunk of -``--workers`` cells, so a 100-cell grid on a 500-video corpus costs roughly -``500 * ceil(100 / workers)`` decodes, not ``500 * 100``. - -Use ``--params "key=v1,v2,v3"`` for enumerated values and ``"key=a:b:s"`` for a numeric -``[a, b]`` range with step ``s`` (inclusive of ``b`` when the step lands there). -Multiple keys are separated by ``;`` and form a Cartesian product. - -Example:: - - python -m benchmark.sweep \\ - --detector detect-content --dataset BBC \\ - --params "threshold=15:35:1;min_scene_len=0.0:1.0:0.1" \\ - --tolerance 0,1 --workers 16 --out sweep.json - -``min_scene_len`` is a :data:`TimecodeLike`: integers are frames, floats are seconds, -strings like ``"0.1s"`` or ``"00:00:00.500"`` also work. Prefer floats so the same -sweep is meaningful across datasets with different framerates. - -Reports the top-10 cells by F1 at each tolerance plus the Pareto front across the two -tolerances. The full grid lives in the JSON output for offline plotting. -""" - -from __future__ import annotations - -import argparse -import itertools -import threading -import time -from pathlib import Path -from typing import Any - -from tqdm import tqdm - -from benchmark._common import ( - DEFAULT_BACKEND, - DETECTORS, - parse_tolerances, - render_table, - write_json, -) -from benchmark.dataset import DATASETS, Dataset, resolve_dataset -from benchmark.evaluator import BenchmarkResult, Prediction, evaluate -from scenedetect import AVAILABLE_BACKENDS, SceneManager, open_video -from scenedetect._fan_out import FanOutVideoStream - -# --------------------------------------------------------------------- # -# Spec language: "key=v1,v2,v3" or "key=a:b:s"; clauses joined by ";". -# --------------------------------------------------------------------- # - - -def _coerce(token: str) -> Any: - """Best-effort scalar coercion. Order: None, bool, int, float, str.""" - t = token.strip() - if t == "None": - return None - if t == "True": - return True - if t == "False": - return False - try: - return int(t) - except ValueError: - pass - try: - return float(t) - except ValueError: - pass - return t - - -def _expand_values(s: str) -> list[Any]: - if ":" in s: - parts = s.split(":") - if len(parts) != 3: - raise ValueError(f"Range spec must be 'start:stop:step', got {s!r}") - a, b, step = _coerce(parts[0]), _coerce(parts[1]), _coerce(parts[2]) - if not all(isinstance(x, (int, float)) for x in (a, b, step)): - raise ValueError(f"Range bounds must be numeric, got {s!r}") - if step == 0: - raise ValueError(f"Range step must be non-zero, got {s!r}") - out: list[Any] = [] - v = a - # Small epsilon to keep an inclusive upper bound robust against float drift. - epsilon = abs(step) * 1e-9 if isinstance(step, float) else 0 - # Direction-aware: support a > b with negative step too. - if step > 0: - while v <= b + epsilon: - out.append(v) - v = v + step - else: - while v >= b - epsilon: - out.append(v) - v = v + step - return out - return [_coerce(v) for v in s.split(",") if v.strip()] - - -def parse_params_spec(spec: str | None) -> dict[str, list[Any]]: - """Parse ``"k1=v1,v2;k2=a:b:s"`` into ``{"k1": [v1, v2], "k2": [...]}``.""" - if not spec: - return {} - out: dict[str, list[Any]] = {} - for clause in spec.split(";"): - clause = clause.strip() - if not clause: - continue - if "=" not in clause: - raise ValueError(f"Param clause missing '=': {clause!r}") - key, _, values = clause.partition("=") - out[key.strip()] = _expand_values(values.strip()) - return out - - -def cartesian_grid(spec: dict[str, list[Any]]) -> list[dict[str, Any]]: - """Expand ``{"k1": [a, b], "k2": [c]}`` into ``[{"k1": a, "k2": c}, {"k1": b, "k2": c}]``.""" - if not spec: - return [{}] - keys = list(spec.keys()) - return [dict(zip(keys, combo, strict=True)) for combo in itertools.product(*spec.values())] - - -# --------------------------------------------------------------------- # -# Per-video fan-out driver -# --------------------------------------------------------------------- # - - -def _run_chunk( - source_path: Path, - backend: str, - detector_cls: type, - chunk: list[dict[str, Any]], -) -> list[tuple[list[int], float]]: - """Drive one decode of ``source_path`` and fan out to ``len(chunk)`` parallel detectors. - - Returns one ``(cuts, elapsed)`` pair per chunk entry. ``elapsed`` is wall-clock per - worker thread and is bound by the slowest detector in the chunk, so it is only a - rough indicator of relative cost. - """ - source = open_video(source_path, backend=backend) - fan = FanOutVideoStream(source, n=len(chunk)) - fan.start() - results: list[tuple[list[int], float]] = [([], 0.0) for _ in chunk] - errors: list[BaseException | None] = [None] * len(chunk) - - def worker(i: int, params: dict[str, Any]) -> None: - try: - stream = fan.stream(i) - detector = detector_cls(**params) - sm = SceneManager() - sm.add_detector(detector) - t0 = time.time() - sm.detect_scenes(video=stream) - elapsed = time.time() - t0 - cuts = [scene[1].frame_num for scene in sm.get_scene_list()] - results[i] = (cuts, elapsed) - except BaseException as exc: - errors[i] = exc - fan.abort() - - threads = [threading.Thread(target=worker, args=(i, p)) for i, p in enumerate(chunk)] - try: - for t in threads: - t.start() - for t in threads: - t.join() - finally: - fan.close() - - first_err = next((e for e in errors if e is not None), None) - if first_err is not None: - raise first_err - return results - - -def _chunked(items: list, size: int) -> list[list]: - return [items[i : i + size] for i in range(0, len(items), size)] - - -def run_sweep( - dataset: Dataset, - detector_name: str, - backend: str, - grid: list[dict[str, Any]], - workers: int, -) -> list[dict[Path, Prediction]]: - """For each cell in ``grid``, return a ``{video_path: Prediction}`` mapping suitable - for :func:`benchmark.evaluator.evaluate`. Cells are evaluated in chunks of - ``workers`` parallel detectors per video decode.""" - detector_cls = DETECTORS[detector_name] - # predictions_by_cell[cell_index][video_path] = Prediction - predictions_by_cell: list[dict[Path, Prediction]] = [{} for _ in grid] - pbar = tqdm(dataset, desc=f"sweep[{detector_name}]") - for sample in pbar: - for chunk_indices in _chunked(list(range(len(grid))), workers): - chunk = [grid[i] for i in chunk_indices] - outputs = _run_chunk(sample.video_file, backend, detector_cls, chunk) - for cell_i, (cuts, elapsed) in zip(chunk_indices, outputs, strict=True): - predictions_by_cell[cell_i][sample.video_file] = Prediction( - predicted_cuts=cuts, - ground_truth=sample.ground_truth, - elapsed=elapsed, - ) - return predictions_by_cell - - -# --------------------------------------------------------------------- # -# Reporting -# --------------------------------------------------------------------- # - - -def _params_str(params: dict[str, Any]) -> str: - return ", ".join(f"{k}={v}" for k, v in sorted(params.items())) - - -def _f1_for(result: BenchmarkResult) -> float: - return result.hard_cuts.f1 - - -def _print_top_n( - label: str, - cells: list[tuple[dict[str, Any], BenchmarkResult]], - n: int = 10, -) -> None: - ranked = sorted(cells, key=lambda c: _f1_for(c[1]), reverse=True)[:n] - rows = [] - for params, result in ranked: - hard = result.hard_cuts - rows.append( - [ - f"{hard.f1 * 100:.2f}", - f"{hard.precision * 100:.2f}", - f"{hard.recall * 100:.2f}", - _params_str(params), - ] - ) - if not rows: - return - print(f"\n## {label} (top {min(n, len(ranked))})\n") - print(render_table(["F1", "Precision", "Recall", "Params"], rows)) - - -def _pareto_front( - cells_at_tols: dict[int, list[tuple[dict[str, Any], BenchmarkResult]]], -) -> list[tuple[dict[str, Any], dict[int, float]]]: - """Return cells that are not dominated by any other cell across the given tolerances. - - Domination: cell A dominates B if F1@tol(A) >= F1@tol(B) for every tol and strictly - greater on at least one. Identical (P, R) cells coexist on the frontier. - """ - tols = sorted(cells_at_tols.keys()) - if not tols: - return [] - n_cells = len(cells_at_tols[tols[0]]) - # Build a parallel array of (params, {tol: f1}) entries. - table: list[tuple[dict[str, Any], dict[int, float]]] = [] - for i in range(n_cells): - params = cells_at_tols[tols[0]][i][0] - f1s = {t: _f1_for(cells_at_tols[t][i][1]) for t in tols} - table.append((params, f1s)) - frontier: list[tuple[dict[str, Any], dict[int, float]]] = [] - for i, (pi, fi) in enumerate(table): - dominated = False - for j, (_, fj) in enumerate(table): - if i == j: - continue - if all(fj[t] >= fi[t] for t in tols) and any(fj[t] > fi[t] for t in tols): - dominated = True - break - if not dominated: - frontier.append((pi, fi)) - return frontier - - -def _print_pareto( - cells_at_tols: dict[int, list[tuple[dict[str, Any], BenchmarkResult]]], -) -> None: - frontier = _pareto_front(cells_at_tols) - if len(frontier) <= 1: - return - tols = sorted(cells_at_tols.keys()) - header = [*(f"F1@{t}" for t in tols), "Params"] - rows = [ - [*(f"{f1s[t] * 100:.2f}" for t in tols), _params_str(params)] - for params, f1s in sorted(frontier, key=lambda x: -x[1][tols[0]]) - ] - print(f"\n## Pareto frontier ({len(rows)} cells)\n") - print(render_table(header, rows)) - - -# --------------------------------------------------------------------- # -# Entry point -# --------------------------------------------------------------------- # - - -def create_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description="Sweep detector parameters on a benchmark dataset." - ) - parser.add_argument( - "--dataset", - type=str, - required=True, - choices=list(DATASETS.keys()), - help=f"Dataset name. One of: {', '.join(DATASETS.keys())}.", - ) - parser.add_argument( - "--detector", - type=str, - required=True, - choices=list(DETECTORS.keys()), - help=f"Detector name. One of: {', '.join(DETECTORS.keys())}.", - ) - parser.add_argument( - "--params", - type=str, - default="", - help=( - "Parameter spec. Clauses separated by ';'. Each clause is either " - "'key=v1,v2,...' (enumerated values) or 'key=start:stop:step' (numeric range, " - "inclusive of stop when it lands on a step). Omitted keys use the detector's " - "default. For time-valued kwargs like 'min_scene_len', use floats (seconds) " - "so the sweep is framerate-independent, e.g. " - "'threshold=15:35:1;min_scene_len=0.0:1.0:0.1'." - ), - ) - parser.add_argument( - "--dataset-root", - type=str, - default=None, - help="Base directory containing per-dataset subfolders. Defaults to 'benchmark'.", - ) - parser.add_argument( - "--backend", - type=str, - default=DEFAULT_BACKEND, - choices=sorted(AVAILABLE_BACKENDS.keys()), - help=f"Video decoding backend (default: {DEFAULT_BACKEND}).", - ) - parser.add_argument( - "--tolerance", - type=str, - default="0,1", - help="Comma-separated frame tolerances (default: 0,1).", - ) - parser.add_argument( - "--workers", - type=int, - default=8, - help=( - "Number of detector instances to drive in parallel from a single video decode " - "(default: 8). Cells beyond --workers are processed in subsequent chunks, each " - "re-decoding the source video. Memory grows with --workers * prefetch frames." - ), - ) - parser.add_argument( - "--quick", - type=int, - nargs="?", - const=10, - default=None, - metavar="N", - help="Score only the first N samples for fast iteration.", - ) - parser.add_argument( - "--out", - type=str, - default=None, - help="Path to write a machine-readable JSON sweep file (one entry per cell).", - ) - return parser - - -def main() -> None: - args = create_parser().parse_args() - tolerances = parse_tolerances(args.tolerance) - if not tolerances: - raise SystemExit("--tolerance must yield at least one value.") - if args.workers < 1: - raise SystemExit("--workers must be at least 1.") - - spec = parse_params_spec(args.params) - grid = cartesian_grid(spec) - if not grid: - raise SystemExit("Empty parameter grid.") - - dataset = resolve_dataset(args.dataset, args.dataset_root) - if len(dataset) == 0: - raise SystemExit( - f"Dataset {args.dataset!r} at {args.dataset_root or 'benchmark'} is empty - " - "check that videos and annotations are present." - ) - if args.quick is not None: - dataset._samples = dataset._samples[: args.quick] - print(f"--quick: limited to first {len(dataset)} samples") - - print( - f"Sweeping {args.detector} on {args.dataset}: " - f"{len(grid)} cells x {len(dataset)} videos " - f"(backend={args.backend}, workers={args.workers})" - ) - - predictions_by_cell = run_sweep(dataset, args.detector, args.backend, grid, args.workers) - - # Score every cell at every tolerance. - cells_at_tols: dict[int, list[tuple[dict[str, Any], BenchmarkResult]]] = { - t: [ - (params, evaluate(preds, tolerance=t)) - for params, preds in zip(grid, predictions_by_cell, strict=True) - ] - for t in tolerances - } - - for t in tolerances: - _print_top_n(f"Best by F1 @ tolerance={t}", cells_at_tols[t]) - if len(tolerances) >= 2: - _print_pareto(cells_at_tols) - - if args.out: - payload = { - "detector": args.detector, - "dataset": args.dataset, - "backend": args.backend, - "workers": args.workers, - "spec": args.params, - "cells": [ - { - "params": params, - "results": {str(t): cells_at_tols[t][i][1].to_dict() for t in tolerances}, - } - for i, params in enumerate(grid) - ], - } - write_json(args.out, payload) - - -if __name__ == "__main__": - main() diff --git a/benchmarks/index.html b/benchmarks/index.html new file mode 100644 index 00000000..2d709483 --- /dev/null +++ b/benchmarks/index.html @@ -0,0 +1,276 @@ + + + + + + + + Benchmarks - PySceneDetect + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+
+
+ +

Benchmarks

+

PySceneDetect's detectors are benchmarked for accuracy against public +shot-boundary-detection corpora. Scoring follows the +TRECVID-SBD convention +(greedy 1-to-1 nearest-neighbor matching with a configurable frame tolerance for hard cuts; +point-in-interval matching for fades), so numbers are comparable to published results. +The benchmark harness, datasets, and full raw results live in +benchmark/ on GitHub.

+

Three datasets are used, chosen to cover very different content:

+
    +
  • BBC Planet Earth - 11 long-form broadcast episodes (hard cuts only)
  • +
  • AutoShot - short-form web/user-generated clips (hard cuts only)
  • +
  • ClipShots - 500 short web clips with hard cuts and typed gradual transitions
  • +
+

Accuracy at default settings

+

Grouped bar chart of hard-cut F1 score per detector and dataset at default settings. AdaptiveDetector leads on BBC (92) and AutoShot (74); HistogramDetector trails, dropping to 20 on ClipShots.

+

Hard cuts, strict frame-exact matching (tolerance 0). F1 cells are shaded by score.

+

BBC Planet Earth

+ + + + + + + +
DetectorRecallPrecisionF1
AdaptiveDetector87.1296.5591.59
ContentDetector84.7088.7786.69
HashDetector92.3075.5683.10
HistogramDetector89.8472.0379.96
ThresholdDetector *0.060.700.11
+ +

AutoShot

+ + + + + + + +
DetectorRecallPrecisionF1
AdaptiveDetector70.5977.4673.86
ContentDetector63.4976.1969.26
HashDetector56.4876.1164.84
HistogramDetector63.2753.2357.82
ThresholdDetector *0.7538.641.47
+ +

ClipShots (hard cuts)

+ + + + + + + +
DetectorRecallPrecisionF1
AdaptiveDetector85.9741.2555.75
ContentDetector81.9342.3655.84
HashDetector81.3430.1443.98
HistogramDetector72.2011.4719.80
ThresholdDetector *0.080.580.14
+ +

ClipShots (fades)

+ + + + + + + +
DetectorRecallPrecisionF1
AdaptiveDetector13.6598.1223.96
ContentDetector26.0398.0441.14
HashDetector18.7794.5331.33
HistogramDetector69.6781.9975.33
ThresholdDetector *5.6999.2410.77
+ +

* ThresholdDetector detects fades to/from black, not shot-to-shot transitions; near-zero +hard-cut scores are expected. Included for completeness.

+

Parameter sweeps

+

Beyond the default values, a sweep over each detector's key parameters shows how +accuracy per dataset changes:

+

Four small-multiple line charts showing hard-cut F1 at 1-frame tolerance versus threshold for detect-content, detect-adaptive, detect-hash, and detect-hist. Each panel has one line per dataset with a dot at that dataset's optimum; BBC and AutoShot peak at lower thresholds than ClipShots in most panels.

+

Dots mark each dataset's optimum within the shown parameter slice. Long-form broadcast content (BBC) +generally prefers lower thresholds than short web clips (ClipShots), so the defaults aim for a +robust middle ground.

+

Grouped bar chart of hard-cut F1 at 1-frame tolerance after parameter tuning. Bars show the best single cross-dataset parameter set per detector and dataset, a black tick marks the v0.7 default, and a dot marks each dataset's own optimum. HistogramDetector shows the largest gap between default and tuned scores, most dramatically on ClipShots (20 vs 48); for ContentDetector and AdaptiveDetector on BBC the default tick sits slightly above the tuned bar.

+

Scored by mean hard-cut F1 at 1-frame tolerance across all three datasets:

+ + + + + + +
DetectorBest mean F1Best parametersv0.7 default
AdaptiveDetector76.3adaptive_threshold=3.5, window_width=3, min_scene_len=0.6sadaptive_threshold=3.0, window_width=2
ContentDetector73.4threshold=31, min_scene_len=0.6sthreshold=27
HashDetector69.8threshold=0.35, size=8threshold=0.395, size=16
HistogramDetector66.3threshold=0.20, bins=128threshold=0.05, bins=256
+ +

Full per-dataset breakdowns are in +benchmark/SWEEP_REPORT.md.

+

Benchmarking

+

See benchmark/README.md +for dataset download instructions and usage.

+
# Score one detector on one dataset:
+python -m benchmark --detector detect-content --dataset BBC
+
+# Grid sweep over detector parameters:
+python -m benchmark.sweep --detector detect-content --dataset BBC \
+    --params "threshold=15:35:1;min_scene_len=0.0:1.0:0.1"
+
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + + diff --git a/changelog/index.html b/changelog/index.html new file mode 100644 index 00000000..4e8e99d3 --- /dev/null +++ b/changelog/index.html @@ -0,0 +1,1108 @@ + + + + + + + + Changelog - PySceneDetect + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+
+
+ +

Releases

+

PySceneDetect 0.7

+

PySceneDetect 0.7.1 (July 21, 2026)

+

PySceneDetect 0.7.1 adds support for concatenating multiple videos, along with several stability and robustness fixes for the PyAV and OpenCV backends.

+

CLI Changes

+
    +
  • [feature] split-video has a new --expand flag: when scenes are detected within a time window (-s/-e), the first output clip is extended back to the start of the video and the last clip is extended forward to the end, so no footage outside the analysis window is dropped #115
  • +
+

API Changes

+
    +
  • [feature] scenedetect.detect() now accepts a backend keyword argument ("opencv", "pyav", or "moviepy") similar to open_video. Defaults to "opencv", matching prior behavior.
  • +
  • [feature] Add expand_scenes_to_bounds() helper in scenedetect.scene_manager to extend a scene list so the first scene starts at a given lower bound and the last scene ends at a given upper bound
  • +
  • [feature] VideoStream now provides a public read-only decode_failures property reporting the number of frames that failed to decode and were skipped (defaults to 0; populated by the OpenCV and PyAV backends)
  • +
  • [feature] Add VideoStreamConcat (scenedetect.backends.concat) which concatenates multiple videos into a single continuous stream with a monotonic PTS timeline; open_video() and detect() now accept a list of paths. VideoStreamConcat.map_span() maps spans of the global timeline back to per-source local times
  • +
  • [bugfix] The PyAV backend (VideoStreamAv) now skips corrupt frames during read() and continues decoding instead of failing, giving up only after 8 consecutive decode failures (matching the OpenCV backend's tolerance behavior)
  • +
  • [bugfix] The PyAV backend now normalizes presentation times by the stream start time, so files with a delayed start (e.g. from edit lists) report the first frame at position 0, consistent with other backends and with seek()
  • +
  • [bugfix] Comparisons between two FrameTimecode objects that both carry exact presentation times (e.g. positions from VFR videos) and share the same frame rate are now performed exactly using pts and time_base instead of rounded frame numbers. Previously, distinct frames in VFR sections could compare equal or fail strict ordering when their times rounded to the same approximate frame number. Comparisons involving frame- or seconds-based timecodes, plain values (int/float/str), or differing frame rates are unchanged
  • +
  • [bugfix] Fix image sequence inputs when using OpenCV 5.0
  • +
+

Packaging

+
    +
  • [general] scenedetect and scenedetect-headless are unchanged: they continue to ship the full program (library + CLI) with opencv-python / opencv-python-headless respectively. Both packages provide the same scenedetect module (install or depend only one)
  • +
  • [feature] Official Docker images are now published to the GitHub Container Registry with the full CLI, all backends, and external tools (ffmpeg, mkvmerge) included, thanks @FNGarvin #537
      +
    • Example usage (process a video in the current directory):
    • +
    +
  • +
+
docker run --rm -v "$(pwd):/files" ghcr.io/breakthrough/pyscenedetect -i /files/video.mp4 detect-adaptive split-video -o /files
+
+
    +
  • [general] The Windows distribution now bundles OpenCV 5.0, PyAV 18, and FFmpeg 8.1.2. The Windows and Docker builds also override Pillow to 12.3.0 for upstream security fixes (moviepy#2553)
  • +
+

General

+
    +
  • [general] Benchmark results are now published on the website (scenedetect.com/benchmarks), including accuracy at default settings and parameter sweep curves for each detector
  • +
+

0.7 (May 3, 2026)

+

PySceneDetect 0.7 is a major breaking release which overhauls how timestamps are handled. This allows PySceneDetect to properly process variable framerate (VFR) videos. A significant amount of technical debt has been addressed, including removal of deprecated or overly complicated APIs.

+

Care was taken to minimize changes for most common API uses, however more advanced use cases may run into breaking changes. Please review the Migration Guide when updating from v0.6. Minimum supported Python version is now Python 3.10.

+

CLI Changes

+
    +
  • [feature] VFR videos are handled correctly by the OpenCV and PyAV backends, and should work correctly with default parameters
  • +
  • [feature] All CLI options which used to accept frame numbers only now accept seconds (e.g. 0.6s) and timecodes (e.g. 00:00:00.600) #531
  • +
  • [feature] New save-fcp command allows exporting in Final Cut Pro format (FCP7/FCPX) #156
  • +
  • [feature] New save-qp command writes a QP file with scene boundary frame numbers, suitable for forcing keyframes at scene cuts in x264/x265 #448
  • +
  • [feature] New save-html command replaces the deprecated export-html; the prior command remains as an alias and emits a deprecation warning #518
  • +
  • [feature] Add save-edl option --start-timecode/-s to provide a custom start timecode for generated EDLs, supports SMPTE HH:MM:SS:FF or 8-digit HHMMSSFF input #515
  • +
  • [bugfix] Fix floating-point precision error in save-otio output where frame values near integer boundaries (e.g. 90.00000000000001) were serialized with spurious precision
  • +
  • [bugfix] Add mitigation for transient OSError in the MoviePy backend as it is susceptible to subprocess pipe races on slow or heavily loaded systems #496
  • +
  • [feature] The MoviePy backend now supports overriding the source frame rate via -f/--frame-rate (and the VideoStreamMoviePy(frame_rate=...) API), bringing it in line with the OpenCV and PyAV backends
  • +
  • [bugfix] detect-threshold cut frame numbers are now backend-deterministic; previously the cut could differ by 1 frame between PyAV and OpenCV when the fade midpoint landed on a .5 rounding boundary (PyAV uses sub-microsecond PTS, OpenCV uses millisecond-truncated CAP_PROP_POS_MSEC)
  • +
  • [breaking] Remove deprecated -d/--min-delta-hsv option from detect-adaptive command (use -c/--min-content-val instead)
  • +
  • [breaking] Rename -f/--framerate to -f/--frame-rate as part of VFR overhaul (legacy --framerate form is preserved as a hidden alias but will be removed in v0.8)
  • +
  • [general] Support SCENEDETECT_DEBUG environment variable to control how exceptions and debugging are handled. Unhandled exceptions and Ctrl+C now produce a logger-formatted error message and exit cleanly with code 1 instead of dumping a raw Python traceback. Set SCENEDETECT_DEBUG=1 to ensure all exceptions are re-raised instead of being logged. In both cases, the program will exit with a non-zero exit code.
  • +
+

API Changes

+

VFR & Timestamp Overhaul:

+
    +
  • Add write_scene_list_edl, write_scene_list_fcpx, write_scene_list_fcp7, and write_scene_list_otio to the scenedetect.output module so save-edl, save-fcp, and save-otio can be invoked directly from Python (previously CLI-only)
  • +
  • write_scene_list_edl accepts an optional start_timecode parameter (SMPTE HH:MM:SS:FF or 8-digit HHMMSSFF) that is added to every event's source and record columns #515
  • +
  • Add new Timecode type to represent frame timings in terms of the video's source timebase
  • +
  • Add time_base and pts properties to FrameTimecode for more accurate timing information
  • +
  • All backends (PyAV, OpenCV, MoviePy) now return PTS-backed timestamps from VideoStream.position
  • +
  • VideoStream.frame_rate now returns Fraction instead of float
  • +
  • Framerates are now stored as rational Fraction values (e.g. Fraction(24000, 1001) instead of 23.976) to avoid float precision loss
  • +
  • Common NTSC rates (23.976, 29.97, 59.94) are automatically detected from float values
  • +
  • FrameTimecode.frame_num is now approximate for VFR video (based on PTS-derived time)
  • +
  • Add frame_rate property (returns exact Fraction) as the canonical replacement for framerate (returns float) in FrameTimecode and VideoStream
      +
    • For CFR sources, both properties represent the same rate, i.e. time_base equals 1 / frame_rate for CFR sources #548
    • +
    +
  • +
  • Add frame_rate keyword argument to open_video() and the VideoStreamCv2, VideoCaptureAdapter, VideoStreamAv, and VideoStreamMoviePy constructors as the canonical replacement for framerate #548; accepts float | Fraction | None. The legacy framerate keyword is retained as a deprecated alias and is ignored when frame_rate is provided
  • +
  • Add equal_frame_rate(other) method as the canonical replacement for equal_framerate(fps)
  • +
+

General:

+
    +
  • Type hints: audit and overhaul: first-party code is now clean with Pyright basic mode, migrated deprecated type hints to comply with PEP 585
  • +
  • Code quality: expand static analysis rules, audit and cleanup existing suppressions
  • +
  • Packaging: modernized to comply with PEP 621, make opencv-python a requirement, add separate scenedetect-headless variant instead
  • +
+

Detector Interface:

+
    +
  • Replace frame_num parameter (int) with timecode (FrameTimecode) in SceneDetector interface #168:
      +
    • The detector interface: SceneDetector.process_frame() and SceneDetector.post_process() (the post_process signature on the abstract base is now consistently typed as FrameTimecode to match its concrete-detector overrides; the prior int annotation did not reflect the actual runtime value)
    • +
    • Statistics: StatsManager.get_metrics(), StatsManager.set_metrics(), and StatsManager.metrics_exist() formally accept either FrameTimecode or int (the int form is retained for compatibility with the deprecated load_from_csv() path, which keys metrics by integer frame number)
    • +
    +
  • +
  • StatsManager.load_from_csv() and save_images() output_dir now accept os.PathLike (e.g. pathlib.Path) in addition to str
  • +
  • SceneManager.detect_scenes() duration and end_time formally accept int (frames), float (seconds), str (timecode), or FrameTimecode - matching the documented and runtime-supported behavior
  • +
  • SceneDetector is now a Python abstract class
  • +
  • SceneDetector instances can now assume they always have frame data to process when process_frame is called
  • +
  • Remove SceneDetector.is_processing_required() method
  • +
  • Remove SceneDetector.stats_manager_required property, no longer required
  • +
  • Remove deprecated SparseSceneDetector interface
  • +
  • Detector min_scene_len and save_images() frame_margin arguments now accept seconds (float) and timecode strings (e.g. "0.6s", "00:00:00.600") in addition to a frame count (int); these are evaluated using the source video's timing for correct behavior on VFR videos #531
  • +
+

Module Reorganization:

+
    +
  • scenedetect.scene_detector moved to scenedetect.detector
  • +
  • scenedetect.frame_timecode moved to scenedetect.common
  • +
  • Image/HTML/CSV export in scenedetect.scene_manager moved to scenedetect.output #463
  • +
  • scenedetect.video_splitter moved to scenedetect.output.video #463
  • +
+

FrameTimecode:

+
    +
  • Add properties to access frame_num, frame_rate, and seconds instead of getter methods
  • +
  • frame_num and frame_rate are now read-only properties (construct a new FrameTimecode to change them)
  • +
  • Remove FrameTimecode.previous_frame() method
  • +
  • Deprecated functionality preserved from v0.6 now uses the warnings module to emit runtime deprecation warnings, these features will be removed in v0.8
  • +
  • Soft-deprecate framerate property and equal_framerate() method via docstring; the legacy forms will continue to work until v0.8 when they will be upgraded to DeprecationWarning before removal in v0.9
  • +
+

Removals:

+
    +
  • Remove deprecated module scenedetect.video_manager, use the scenedetect.open_video() function instead
  • +
  • Remove deprecated parameters base_timecode and video_manager from various functions
  • +
  • Remove deprecated SceneManager.get_event_list() method
  • +
  • Remove deprecated AdaptiveDetector.get_content_val() method (use StatsManager instead)
  • +
  • Remove deprecated AdaptiveDetector constructor arg min_delta_hsv (use min_content_val instead)
  • +
  • Remove advance parameter from VideoStream.read()
  • +
  • Remove SceneDetector.stats_manager_required property, no longer required
  • +
  • SceneDetector is now a Python abstract class
  • +
+

Windows Distribution

+
    +
  • [general] Updates to Windows distributions:
      +
    • av 14.2.0 -> 17.0.1
    • +
    • click 8.1.8 -> 8.2.1
    • +
    • imageio-ffmpeg 0.6.0
    • +
    • moviepy 2.1.2 -> 2.2.1
    • +
    • numpy 2.2.3 -> 2.4.4
    • +
    • opencv-python-headless 4.11.0.86 -> 4.13.0.92
    • +
    • platformdirs 4.3.6 -> 4.9.6
    • +
    • tqdm 4.67.1 -> 4.67.3
    • +
    • ffmpeg 8.0 -> 8.1
    • +
    +
  • +
  • [general] Reduced size of Windows distribution without affecting functionality
  • +
  • [bugfix] Pressing Ctrl+C during scene detection in the bundled distribution now exits cleanly instead of surfacing the PyInstaller bootloader traceback
  • +
+
+

PySceneDetect 0.6

+

PySceneDetect 0.6.7.1 (September 24, 2025)

+

Re-release of the Python package that fixes dependency version pinning.

+

PySceneDetect 0.6.7 (August 24, 2025)

+

Minor update to fix issues with importing EDL files into DaVinci Resolve and other editors.

+

Changelog

+
    +
  • [bugfix] Fix save-edl end timestamp being too short by 1 frame #516
  • +
  • [general] Updates to Windows distributions:
      +
    • ffmpeg 7.1 -> 8.0
    • +
    +
  • +
+

PySceneDetect 0.6.6 (March 9, 2025)

+

PySceneDetect v0.6.6 introduces new output formats, which improve compatibility with popular video editors (e.g. DaVinci Resolve).

+

Changelog

+
    +
  • [feature] New save-otio command supports saving scenes in OTIO format #497
  • +
  • [feature] New save-edl command supports saving scenes in EDL format CMX 3600 #495
  • +
  • [bugfix] Fix incorrect help entries for short-form arguments which suggested invalid syntax #493
  • +
  • [bugfix] Fix crash when using split-video with -m/--mkvmerge option #473
  • +
  • [bugfix] Fix incorrect default filename template for split-video command with -m/--mkvmerge option
  • +
  • [bugfix] Fix inconsistent filenames when using split_video_mkvmerge()
  • +
  • [bugfix] Ensure auto-rotation is always enabled for VideoStreamCv2 as workaround for (opencv#26795)[https://github.com/opencv/opencv/issues/26795]
  • +
  • [general] The export-html command is now deprecated, use save-html instead
  • +
  • [general] Updates to Windows distributions:
      +
    • av 13.1.0 -> 14.2.0
    • +
    • click 8.1.7 -> 8.1.8
    • +
    • imageio-ffmpeg 0.5.1 -> 0.6.0
    • +
    • moviepy 2.1.1 -> 2.1.2
    • +
    • numpy 2.1.3 -> 2.2.3
    • +
    • opencv-python 4.10.0.84 -> 4.11.0.86
    • +
    +
  • +
  • [general] Windows download URLs for standalone ZIP distribution no longer have portable suffix
  • +
+

PySceneDetect 0.6.5 (November 24, 2024)

+

This release brings crop support, performance improvements to save-images, lots of bugfixes, and improved compatibility with MoviePy 2.0+.

+

Changelog

+
    +
  • [feature] Add ability to crop input video before processing #302 #449
      +
    • [cli] Add --crop option to scenedetect command and config file to crop video frames before scene detection
    • +
    • [api] Add crop property to SceneManager to crop video frames before scene detection
    • +
    +
  • +
  • [feature] Add ability to configure CSV separators for rows/columns in config file #423
  • +
  • [feature] Add new --show flag to export-html command to launch browser after processing #442
  • +
  • [improvement] Add new threading option to save-images/save_images() #456
      +
    • Enabled by default, offloads image encoding and disk IO to separate threads
    • +
    • Improves performance by up to 50% in some cases
    • +
    +
  • +
  • [improvement] The export-html command now implicitly invokes save-images with default parameters
      +
    • The output of the export-html command will always use the result of the save-images command that precedes it
    • +
    +
  • +
  • [improvement] save_to_csv now works with paths from pathlib
  • +
  • [api] The save_to_csv function now works correctly with paths from the pathlib module
  • +
  • [api] Add col_separator and row_separator args to write_scene_list function in scenedetect.scene_manager
  • +
  • [api] The MoviePy backend now works with MoviePy 2.0+
  • +
  • [bugfix] Fix SyntaxWarning due to incorrect escaping #400
  • +
  • [bugfix] Fix ContentDetector crash when using callbacks #416 #420
  • +
  • [bugfix] Fix save-images/save_images() not working correctly with UTF-8 paths #450
  • +
  • [bugfix] Fix crash when using save-images/save_images() with OpenCV backend #455
  • +
  • [bugfix] Fix new detectors not working with default-detector config option
  • +
  • [general] Timecodes of the form MM:SS[.nnn] are now processed correctly #443
  • +
  • [general] Updates to Windows distributions:
      +
    • The MoviePy backend is now included with Windows distributions
    • +
    • Python 3.9 -> Python 3.13
    • +
    • PyAV 10 -> 13.1.0
    • +
    • OpenCV 4.10.0.82 -> 4.10.0.84
    • +
    • Ffmpeg 6.0 -> 7.1
    • +
    +
  • +
+

Python Distribution Changes

+
    +
  • v0.6.5.1 - Fix compatibility issues with PyAV 14+ #466
  • +
  • v0.6.5.2 - Fix for AttributeError: module 'cv2' has no attribute 'Mat' #468
  • +
+

0.6.4 (June 10, 2024)

+

Includes new histogram and perceptual hash based detectors (thanks @wjs018 and @ash2703), adds flash filter to content detector, and includes various bugfixes. Below shows the scores of the new detectors normalized against detect-content for comparison on a difficult segment with 3 cuts:

+

comparison of new detector scores

+

Feedback on the new detection methods and their default values is most welcome. Thanks to everyone who contributed for their help and support!

+

Changelog

+
    +
  • [feature] New detectors:
  • +
  • detect-hist / HistogramDetector #295 #53
  • +
  • detect-hash / HashDetector #290
  • +
  • [feature] Add flash suppression filter for detect-content / ContentDetector (enabled by default) #35 #53
      +
    • Reduces number of cuts generated during strobing or flashing effects
    • +
    • Can be configured using --filter-mode option
    • +
    • --filter-mode = merge (new default) merges consecutive scenes shorter than min-scene-len
    • +
    • --filter-mode = suppress (previous default) disables generating new scenes until min-scene-len has passed
    • +
    +
  • +
  • [feature] Add more templates for save-images filename customization: $TIMECODE, $FRAME_NUMBER, $TIMESTAMP_MS (thanks @Veldhoen0) #395
  • +
  • [bugfix] Remove extraneous console output when using --drop-short-scenes
  • +
  • [bugfix] Fix scene lengths being smaller than min-scene-len when using detect-adaptive / AdaptiveDetector with large values of --frame-window
  • +
  • [bugfix] Fix crash when decoded frames have incorrect resolution and log error instead #319
  • +
  • [bugfix] Update default ffmpeg stream mapping from -map 0 to -map 0:v:0 -map 0:a? -map 0:s? #392
  • +
+

0.6.3 (March 9, 2024)

+

In addition to some performance improvements with the load-scenes command, this release of PySceneDetect includes a significant amount of bugfixes. Thanks to everyone who contributed to the release, including those who filed bug reports and helped with debugging!

+

Program Changes:

+
    +
  • [bugfix] Fix crash for some WebM videos when using save-images with --backend pyav #355
  • +
  • [bugfix] Correct --duration and --end for presentation time when specified as frame numbers #341
  • +
  • [bugfix] Progress bar now has correct frame accounting when --duration or --end are set #341
  • +
  • [bugfix] Only allow load-scenes to be specified once, and disallow with other detect-* commands #347
  • +
  • [bugfix] Disallow -s/--start being larger than -e/--end for the time command
  • +
  • [bugfix] Fix detect-adaptive not respecting --min-scene-len for the first scene
  • +
  • [general] Comma-separated timecode list is now only printed when the list-scenes command is specified #356
  • +
  • [general] Several changes to [list-scenes] config file options:
  • +
  • Add display-scenes and display-cuts options to control output
  • +
  • Add cut-format to control formatting of cut points #349
      +
    • Valid values: frames, timecode, seconds
    • +
    +
  • +
  • [general] Increase progress bar indent to improve visibility and visual alignment
  • +
  • [improvement] The s suffix for setting timecode values in seconds is no longer required (values without decimal places are still interpreted as frame numbers)
  • +
  • [improvement] load-scenes now skips detection, generating output much faster #347 (thanks @wjs018 for the initial implementation)
  • +
+

API Changes:

+
    +
  • [bugfix] Fix AttributeError thrown when accessing aspect_ratio on certain videos using VideoStreamAv #355
  • +
  • [bugfix] Fix circular imports due to partially initialized module for some development environments #350
  • +
  • [bugfix] Fix SceneManager.detect_scenes warning when duration or end_time are specified as timecode strings #346
  • +
  • [bugfix] Ensure correct string conversion behavior for FrameTimecode when rounding is enabled #354
  • +
  • [bugfix] Fix AdaptiveDetector not respecting min_scene_len for the first scene
  • +
  • [feature] Add output_dir argument to split_video_ffmpeg and split_video_mkvmerge functions to set output directory #298
  • +
  • [feature] Add formatter argument to split_video_ffmpeg to allow formatting filenames via callback #359
  • +
  • [general] The frame_img argument to SceneDetector.process_frame() is now required
  • +
  • [general] Remove TimecodeValue from scenedetect.frame_timecode (use typing.Union[int, float, str])
  • +
  • [general] Remove MotionDetector and scenedetect.detectors.motion_detector module (will be reintroduced after SceneDetector interface is stable)
  • +
  • [improvement] scenedetect.stats_manager module improvements:
  • +
  • The StatsManager.register_metrics() method no longer throws any exceptions
  • +
  • Add StatsManager.metric_keys property to query registered metric keys
  • +
  • Deprecate FrameMetricRegistered and FrameMetricNotRegistered exceptions (no longer used)
  • +
  • [improvement] When converting strings representing seconds to FrameTimecode, the s suffix is now optional, and whitespace is ignored (note that values without decimal places are still interpreted as frame numbers)
  • +
  • [improvement] The VideoCaptureAdapter in scenedetect.backends.opencv now attempts to report duration if known
  • +
+

0.6.2 (July 23, 2023)

+

Includes new load-scenes command, ability to specify a default detector, PyAV 10 support, and several bugfixes. Minimum supported Python version is now Python 3.7.

+

Command-Line Changes:

+
    +
  • [feature] Add load-scenes command to load cuts from list-scenes CSV output #235
  • +
  • [feature] Use detect-adaptive by default if a detector is not specified #329
  • +
  • Default detector can be set by config file with the default-detector option under [global]
  • +
  • [bugfix] Fix -d/--duration and -e/--end options of time command consuming one extra frame #307
  • +
  • [bugfix] Fix incorrect end timecode for final scene when last frame of video is a new scene #307
  • +
  • [bugfix] Expand $VIDEO_NAME before creating output directory for -f/--filename option of split-video, now allows absolute paths
  • +
  • [general] Rename ThresholdDetector (detect-threshold) metric delta_rgb metric to average_rgb
  • +
  • [general] -l/--logfile always produces debug logs now
  • +
  • [general] Remove -a/--all flag from scenedetect version command, now prints all information by default (can still call scenedetect for version number alone)
  • +
  • [general] Add -h/--help options globally and for each command
  • +
  • [general] Remove all option from scenedetect help command (can now call scenedetect help for full reference)
  • +
+

General:

+
    +
  • [feature] Add ability to specify method (floor/ceiling) when creating ThresholdDetector, allows fade to white detection #143
  • +
  • [general] Minimum supported Python version is now Python 3.7
  • +
  • [general] Add support for PyAV 10.0 #292
  • +
  • [general] Use platformdirs package instead of appdirs #309
  • +
  • [bugfix] Fix end_time always consuming one extra frame #307
  • +
  • [bugfix] Fix incorrect end timecode for last scene when start_in_scene is True or the final scene contains a single frame #307
  • +
  • [bugfix] Fix MoviePy read next frame #320
  • +
  • [bugfix] Template replacement when generating output now allows lower-case letters to be used as separators in addition to other characters
  • +
  • [api] Make some public functions/methods private (prefixed with _):
  • +
  • get_aspect_ratio function in scenedetect.backends.opencv
  • +
  • mean_pixel_distance and estimated_kernel_size functions in scenedetect.detectors.content_detector
  • +
  • compute_frame_average function in scenedetect.detectors.threshold_detector
  • +
  • scenedetect.cli and scenedetect.thirdparty modules
  • +
  • [api] Remove compute_downscale_factor in scenedetect.video_stream (use scenedetect.scene_manager.compute_downscale_factor instead)
  • +
  • [dist] Updated dependencies in Windows distributions: ffmpeg 6.0, PyAV 10, OpenCV 4.8, removed mkvmerge
  • +
+

Project Updates

+
    +
  • Website and documentation is now hosted on Github Pages, documentation can be found at scenedetect.com/docs
  • +
  • Windows and Linux builds are now done on Github Actions, add OSX builds as well
  • +
  • Build matrix has been updated to support Python 3.7 through 3.11 for all operating systems for Python distributions
  • +
  • Windows portable builds have been moved to Github Actions, signed builds/installer is still done on Appveyor
  • +
  • Windows distributions no longer include mkvmerge (can still download for Windows here)
  • +
+

0.6.1 (November 28, 2022)

+

Includes MoviePy support, edge detection capability for fast cuts, and several enhancements/bugfixes.

+

Changelog

+

Command-Line Changes:

+
    +
  • [feature] Add moviepy backend wrapping the MoviePy package, uses ffmpeg binary on the system for video decoding
  • +
  • [feature] Edge detection can now be enabled with detect-content and detect-adaptive to improve accuracy in some cases, especially under lighting changes, see new -w/--weights option for more information
      +
    • A good starting point is to place 100% weight on the change in a frame's hue, 50% on saturation change, 100% on luma (brightness) change, and 25% on change in edges, with a threshold of 32: +detect-adaptive -w 1.0 0.5 1.0 0.25
    • +
    • Edge differences are typically larger than other components, so you may need to increase -t/--threshold higher when increasing the edge weight (the last component) with detect-content, for example:detect-content -w 1.0 0.5 1.0 0.25 -t 32`
    • +
    • May be enabled by default in the future once it has been more thoroughly tested, further improvements for detect-content are being investigated as well (e.g. motion compensation, flash suppression)
    • +
    +
  • +
  • Short-form of detect-content option --frame-window has been changed from -w to -f to accommodate this change
  • +
  • [enhancement] Progress bar now displays number of detections while processing, no longer conflicts with log message output
  • +
  • [enhancement] When using ffmpeg to split videos, -map 0 has been added to the default arguments so other audio tracks are also included when present (#271)
  • +
  • [enhancement] Add -a flag to version command to print more information about versions of dependencies/tools being used
  • +
  • [enhancement] The resizing method used used for frame downscaling or resizing can now be set using a config file, see [global] option downscale-method and [save-images] option scale-method
  • +
  • [other] Linear interpolation is now used as the default downscaling method (previously was nearest neighbor) for improved edge detection accuracy
  • +
  • [other] Add -c/--min-content-val argument to detect-adaptive, deprecate -d/--min-delta-hsv
  • +
+

General:

+
    +
  • [general] Recommend detect-adaptive over detect-content
  • +
  • [feature] Add new backend VideoStreamMoviePy using the MoviePy package`
  • +
  • [feature] Add edge detection to ContentDetector and AdaptiveDetector (#35)
      +
    • Add ability to specify content score weights of hue, saturation, luma, and edge differences between frames
    • +
    • Default remains as 1.0, 1.0, 1.0, 0.0 so there is no change in behavior
    • +
    • Kernel size used for improving edge overlap can also be customized
    • +
    +
  • +
  • [feature] AdaptiveDetector no longer requires a StatsManager and can now be used with frame_skip (#283)
  • +
  • [bugfix] Fix scenedetect.detect() throwing TypeError when specifying stats_file_path
  • +
  • [bugfix] Fix off-by-one error in end event timecode when end_time was set (reported end time was always one extra frame)
  • +
  • [bugfix] Fix a named argument that was incorrect (#299)
  • +
  • [enhancement] Add optional start_time, end_time, and start_in_scene arguments to scenedetect.detect() (#282)
  • +
  • [enhancement] Add -map 0 option to default arguments of split_video_ffmpeg to include all audio tracks by default (#271)
  • +
  • [docs] Add example for using a callback (#273)
  • +
  • [enhancement] Add new VideoCaptureAdapter to make existing cv2.VideoCapture objects compatible with a SceneManager (#276)
      +
    • Primary use case is for handling input devices/webcams and gstreamer pipes, see updated examples
    • +
    • Files, image sequences, and network streams/URLs should continue to use VideoStreamCv2
    • +
    +
  • +
  • [api] The SceneManager methods get_cut_list() and get_event_list() are deprecated, along with the base_timecode argument
  • +
  • [api] The base_timecode argument of get_scenes_from_cuts() in scenedetect.stats_manager is deprecated (the signature of this function has been changed accordingly)
  • +
  • [api] Rename AdaptiveDetector constructor parameter min_delta_hsv to `min_content_val
  • +
  • [general] The default crf for split_video_ffmpeg has been changed from 21 to 22 to match command line default
  • +
  • [enhancement] Add interpolation property to SceneManager to allow setting method of frame downscaling, use linear interpolation by default (previously nearest neighbor)
  • +
  • [enhancement] Add interpolation argument to save_images to allow setting image resize method (default remains bicubic)
  • +
+

0.6 (May 29, 2022)

+

PySceneDetect v0.6 is a major breaking change including better performance, configuration file support, and a more ergonomic API. The new minimum Python version is now 3.6. See the Migration Guide for information on how to port existing applications to the new API. Most users will see performance improvements after updating, and changes to the command-line are not expected to break most workflows.

+

The main goals of v0.6 are reliability and performance. To achieve this required several breaking changes. The video input API was refactored, and many technical debt items were addressed. This should help the eventual transition to the first planned stable release (v1.0) where the goal is an improved scene detection API.

+

Both the Windows installer and portable distributions now include signed executables. Many thanks to SignPath, AppVeyor, and AdvancedInstaller for their support.

+

Changelog

+

Overview:

+
    +
  • Major performance improvements on multicore systems
  • +
  • Configuration file support via command line option or user settings folder
  • +
  • Support for multiple video backends, PyAV is now supported in addition to OpenCV
  • +
  • Breaking API changes to VideoManager (replaced with VideoStream), StatsManager, and save_images()
      +
    • See the Migration Guide for details on how to update from v0.5.x
    • +
    • A backwards compatibility layer has been added to prevent most applications from breaking, will be removed in a future release
    • +
    +
  • +
  • Support for Python 2.7 has been dropped, minimum supported Python version is 3.6
  • +
  • Support for OpenCV 2.x has been dropped, minimum OpenCV version is 3.x
  • +
  • Windows binaries are now signed, thanks SignPath.io (certificate by SignPath Foundation)
  • +
+

Command-Line Changes:

+
    +
  • Configuration files are now supported, see documentation for details
      +
    • Can specify config file path with -c/--config, or create a scenedetect.cfg file in your user config folder
    • +
    +
  • +
  • Frame numbers are now 1-based, aligning with most other tools (e.g. ffmpeg) and video editors (#265)
  • +
  • Start/end frame numbers of adjacent scenes no longer overlap (#264)
      +
    • End/duration timecodes still include the frame's presentation time
    • +
    +
  • +
  • Add --merge-last-scene option to merge last scene if shorter than --min-scene-len
  • +
  • Add -b/--backend option to use a specific video decoding backend
      +
    • Supported backends are opencv and pyav
    • +
    • Run scenedetect help to see a list of backends available on the current system
    • +
    • Both backends are included with Windows builds
    • +
    +
  • +
  • split-video command:
      +
    • -c/--copy now uses ffmpeg instead of mkvmerge (#77, #236)
    • +
    • Add -m/--mkvmerge flag to use mkvmerge instead of ffmpeg (#77)
    • +
    • Long name for -a has been changed to --args (from --override-args)
    • +
    +
  • +
  • detect-adaptive command:
      +
    • --drop-short-scenes now works properly with detect-adaptive
    • +
    +
  • +
  • detect-content command:
      +
    • Default threshold -t/--threshold lowered to 27 to be more sensitive to shot changes (#246)
    • +
    • Add override for global -m/--min-scene-len option
    • +
    +
  • +
  • detect-threshold command:
      +
    • Remove -p/--min-percent and -b/--block-size options
    • +
    • Add override for global -m/--min-scene-len option
    • +
    +
  • +
  • save-images command now works when -i/--input is an image sequences
  • +
  • Default backend (OpenCV) is more robust to video decoder failures
  • +
  • -i/--input may no longer be specified multiple times, if required use an external tool (e.g. ffmpeg, mkvmerge) to perform concatenation before processing
  • +
  • -s/--stats no longer loads existing statistics and will overwrite any existing files
  • +
  • -l/--logfile now respects -o/--output
  • +
  • -v/--verbosity now takes precedence over -q/--quiet
  • +
+

API Changes:

+
    +
  • New detect() function performs scene detection on a video path, see example here
  • +
  • New open_video() function to handle video input, see example here
  • +
  • split_video_ffmpeg() and split_video_mkvmerge() now take a single path as input
  • +
  • save_images() no longer accepts downscale_factor
      +
    • Use scale or height/width arguments to resize images
    • +
    +
  • +
  • New VideoStream replaces VideoManager (#213)
      +
    • Supports both OpenCV (VideoStreamCv2) and PyAV (VideoStreamAv)
    • +
    • Improves video seeking invariants, especially around defining what frames 0 and 1 mean for different time properties (frame_number is 1-based whereas position is 0-based to align with PTS)
    • +
    • See test_time_invariants in tests/test_video_stream.py as a reference of specific behaviours
    • +
    +
  • +
  • Changes to SceneManager:
      +
    • detect_scenes() now performs video decoding in a background thread, improving performance on most systems
    • +
    • SceneManager is now responsible for frame downscaling via the downscale/auto_downscale properties
    • +
    • detect_scenes() no longer shows a progress bar by default, set show_progress=True to restore the previous behaviour
    • +
    • clear() now clears detectors, as they may be stateful
    • +
    • get_scene_list() now returns an empty list if there are no detected cuts, specify start_in_scene=True for previous behavior (one scene spanning the entire input)
    • +
    +
  • +
  • Changes to StatsManager:
      +
    • save_to_csv() now accepts a path or an open file handle
    • +
    • base_timecode argument has been removed from save_to_csv()
    • +
    • load_from_csv() is now deprecated and will be removed in v1.0
    • +
    +
  • +
  • Changes to FrameTimecode:
      +
    • Use rounding instead of truncation when calculating frame numbers to fix incorrect round-trip conversions and improve accuracy (#268)
    • +
    • Fix previous_frame() generating negative frame numbers in some cases
    • +
    • FrameTimecode objects can now perform arithmetic with formatted strings, e.g. 'HH:MM:SS.nnn'
    • +
    +
  • +
  • Merged constants MAX_FPS_DELTA and MINIMUM_FRAMES_PER_SECOND_DELTA_FLOAT in scenedetect.frame_timecode into new MAX_FPS_DELTA constant
  • +
  • video_manager parameter has been removed from the AdaptiveDetector constructor
  • +
  • split_video_ffmpeg and split_video_mkvmerge function arguments have been renamed and defaults updated:
      +
    • suppress_output is now show_output, default is False
    • +
    • hide_progress is now show_progress, default is False
    • +
    +
  • +
  • block_size argument has been removed from the ThresholdDetector constructor
  • +
  • calculate_frame_score method of ContentDetector has been renamed to _calculate_frame_score, use new module-level function of the same name instead
  • +
  • get_aspect_ratio has been removed from scenedetect.platform (use the aspect_ratio property of a VideoStream instead)
  • +
  • Backwards compatibility with v0.5 to avoid breaking most applications on release while still allowing performance improvements
  • +
+

Python Distribution Changes

+
    +
  • v0.6.0.3 - Fix missing package description
  • +
  • v0.6.0.2 - Improve error messaging when OpenCV is not installed
  • +
  • v0.6.0.1 - Fix original v0.6 release requiring av to run the scenedetect command
  • +
+

Known Issues

+
    +
  • URL inputs are not supported by the save-images or split-video commands
  • +
  • Variable framerate videos (VFR) are not fully supported, and will yield incorrect timestamps (#168)
  • +
  • The detect-threshold option -l/--add-last-scene cannot be disabled
  • +
  • Due to a switch from EXE to MSI for the Windows installer, you may have to uninstall older versions first before installing v0.6
  • +
+
+

PySceneDetect 0.5

+

0.5.6.1 (October 11, 2021)

+
    +
  • Fix crash when using detect-content or detect-adaptive with latest version of OpenCV (thanks @bilde2910)
  • +
+

0.5.6 (August 15, 2021)

+
    +
  • New detection algorithm: detect-adaptive which works similar to detect-content, but with reduced false negatives during fast camera movement (thanks @scarwire and @wjs018)
  • +
  • Images generated by save-images can now be resized via the command line
  • +
  • Statsfiles now work properly with detect-threshold
  • +
  • Removed the -p/--min-percent option from detect-threshold
  • +
  • Add new option -l/--luma-only to detect-content/detect-adaptive to only consider brightness channel (useful for greyscale videos)
  • +
+

Changelog

+
    +
  • [feature] New adaptive content detector algorithm detect-adaptive (#153, thanks @scarwire and @wjs018)
  • +
  • [feature] Images generated with the save-images command (scene_manager.save_images() function in the Python API) can now be scaled or resized (#160 and PR #203, thanks @wjs018)
      +
    • Images can be resized by a constant scaling factory using -s/--scale (e.g. --scale 0.5 shrinks the height/width by half)
    • +
    • Images can be resized to a specified height (-h/--height) and/or width (-w/--width), in pixels; if only one is specified, the aspect ratio of the original video is kept
    • +
    +
  • +
  • [api] Calling seek() on a VideoManager will now respect the end time if set
  • +
  • [api] The split_video_ functions now return the exit code of invoking ffmpeg or mkvmerge (#209, thanks @AdrienLF)
  • +
  • [api] Removed the min_percent argument from ThresholdDetector as was not providing any performance benefit for the majority of use cases (#178)
  • +
  • [bugfix] The detect-threshold command now works properly with a statsfile (#211, thanks @jeremymeyers)
  • +
  • [bugfix] Fixed crash due to unhandled TypeError exception when using non-PyPI OpenCV packages from certain Linux distributions (#220)
  • +
  • [bugfix] A warning is now displayed for videos which may not be decoded correctly, esp. VP9 (#86, thanks @wjs018)
  • +
  • [api] A named logger is now used for both API and CLI logging instead of the root logger (#205)
  • +
+

Known Issues

+
    +
  • Variable framerate videos (VFR) are not fully supported, and will yield incorrect timestamps (#168)
  • +
  • The -l/--add-last-scene option in detect-threshold cannot be disabled
  • +
  • Image sequences or URL inputs are not supported by the save-images or split-video commands (in v0.6 save-images works with image sequences)
  • +
  • Due to the use of truncation for frame number calculation, FrameTimecode objects may be off-by-one when constructed using a float value (#268, fixed in v0.6)
  • +
+

0.5.5 (January 17, 2021)

+
    +
  • One of the last major updates before transitioning to the new v0.6.x API
  • +
  • The --min-scene-len/-m option is now global rather than per-detector
  • +
  • There is a new global option --drop-short-scenes to go along with -m
  • +
  • Removed first row from statsfiles so it is a valid CSV file
  • +
  • The progress bar now correctly resizes when the terminal is resized
  • +
  • Image sequences and URLs are now supported for input via the CLI/API
  • +
  • Images exported using the save-images command are now resized to match the display aspect ratio
  • +
  • A new flag -s/--skip-cuts has been added to the list-scenes command to allow standardized processing
  • +
  • The functionality of save-images is now accessible via the Python API through the save_images() function in scenedetect.scene_manager
  • +
  • Under the save-images command, renamed --image-frame-margin to --frame-margin, added short option -m, and increased the default value from 0 to 1 due to instances of the last frame of a video being occasionally missed (set -m 0 to restore original behaviour)
  • +
+

Changelog

+
    +
  • [bugfix] Allow image sequences and URLs to be used as inputs (#152 and #188)
  • +
  • [bugfix] Pixel aspect ratio is now applied when using save-images (#195)
  • +
  • [cli] Renamed --image-frame-margin to --frame-margin in save-images command, added short option -m as alias
  • +
  • [bugfix] Fix save-images command not saving the last frame by modifying seeking, as well as increasing default of --frame-margin from 0 to 1
  • +
  • [cli] Make --min-scene-len a global option rather than per-detector (#131, thanks @tonycpsu)
  • +
  • [feature] Added --drop-short-scenes option to remove all scenes smaller than --min-scene-len, instead of merging them
  • +
  • [cli] Add -s/--skip-cuts option to list-scenes command to allow outputting a scene list CSV file as compliant with RFC 4180 (#136)
  • +
  • [enhancement] Removed first row from statsfile to comply with RFC 4180, includes backwards compatibility so existing statsfiles can still be loaded (#136)
  • +
  • [api] Add argument include_cut_list to write_scene_list method in SceneManager to support #136
  • +
  • [api] Removed unused argument base_timecode from StatsManager.load_from_csv() method
  • +
  • [api] Make the base_timecode argument optional on the SceneManager methods get_scene_list(), get_cut_list(), and get_event_list() (#173)
  • +
  • [api] Support for live video stream callbacks by adding new callback argument to the detect_scenes() method of SceneManager (#5, thanks @mhashim6)
  • +
  • [bugfix] Fix unhandled exception causing improper error message when a video fails to load on non-Windows platforms (#192)
  • +
  • [enhancement] Enabled dynamic resizing for progress bar (#193)
  • +
  • [enhancement] Always output version number via logger to assist with debugging (#171)
  • +
  • [bugfix] Resolve RuntimeWarning when running as module (#181)
  • +
  • [api] Add save_images() function to scenedetect.scene_manager module which exposes the same functionality as the CLI save-images command (#88)
  • +
  • [api] Removed close_captures() and release_captures() functions from scenedetect.video_manager module
  • +
+

Known Issues

+
    +
  • Certain non-PyPI OpenCV packages may cause a crash with the message TypeError: isinstance() arg 2 must be a type or tuple of types - as a workaround, install the Python OpenCV package by running pip install scenedetect[opencv] (#220)
  • +
  • Image sequences or URL inputs are not supported by the save-images or split-video commands
  • +
  • Variable framerate videos (VFR) are not fully supported, and will yield incorrect timestamps (#168)
  • +
+

0.5.4 (September 14, 2020)

+
    +
  • Improved performance when using time and save-images commands
  • +
  • Improved performance of detect-threshold when using a small minimum percent
  • +
  • Fix crash when using detect-threshold with a statsfile
  • +
  • Fix crash when using save-images command under Python 2.7
  • +
  • Support for Python 3.3 and 3.4 has been deprecated (see below)
  • +
+

Changelog

+
    +
  • [bugfix] fix detect-threshold crash when using statsfile (#122)
  • +
  • [bugfix] fix save-images command under Python 2.7 (#174, thanks @santiagodemierre)
  • +
  • [bugfix] gracefully exit and show link to FAQ when number of scenes is too large to split with mkvmerge on Windows (see #164
  • +
  • [enhancement] Improved seeking performance, greatly improves performance of the time and save-images commands (#98 and PR #163 - thanks @obroomhall)
  • +
  • [enhancement] improve detect-threshold performance when min-percent is less than 50%
  • +
  • [bugfix] Fixed issue where video loading would fail silently due to multiple audio tracks (#179)
  • +
  • [general] Made tqdm a regular requirement and not an extra (#180)
  • +
  • [general] Support for Python 3.3 and 3.4 has been deprecated. Newer builds may still work on these Python versions, but future releases are not tested against these versions. This decision was made as part of #180
  • +
+

Known Issues

+
    +
  • Variable framerate videos are not supported properly currently (#168), a warning may be added in the next release to indicate when a VFR video is detected, until this can be properly resolved (#168)
  • +
+

0.5.3 (July 12, 2020)

+
    +
  • Resolved long-standing bug where split-video command would duplicate certain frames at the beginning/end of the output (#93)
  • +
  • This was determined to be caused by copying (instead of re-encoding) the audio track, causing extra frames to be brought in when the audio samples did not line up on a frame boundary (thank you @joshcoales for your assistance)
  • +
  • Default behavior is to now re-encode audio tracks using the aac codec when using split-video (it can be overridden in both the command line and Python interface)
  • +
  • Improved timestamp accuracy when using split-video command to further reduce instances of duplicated or off-by-one frame issues
  • +
  • Fixed application crash when using the -l/--logfile argument
  • +
+

Changelog

+
    +
  • [bugfix] Changed default audio codec from 'copy' to 'aac' when splitting scenes with ffmpeg to reduce frequency of frames from next scene showing up at the end of the current one when split using ffmpeg (see #93, #159, and PR #166 - thank you everyone for your assistance, especially joshcoales, amvscenes, jelias, and typoman). If this still occurs, please provide any information you can by filing a new issue on Github.
  • +
  • [enhancement] video_splitter module now has completed documentation
  • +
  • [bugfix] improve timestamp accuracy using the split-video command due to timecode formatting
  • +
  • [bugfix] fix crash when supplying -l/--logfile argument (see #169, thanks @typoman)
  • +
+

Known Issues

+
    +
  • Seeking through long videos is inefficient, causing the time and save-images command to take a long time to run. This will be resolved in the next release (see #98)
  • +
  • The save-images command causes PySceneDetect to crash under Python 2.7 (see #174)
  • +
  • Using detect-threshold with a statsfile causes PySceneDetect to crash (see #122)
  • +
  • Variable framerate videos are not supported properly currently (#168), a warning may be added in the next release to indicate when a VFR video is detected, until this can be properly resolved (#168)
  • +
  • Videos with multiple audio tracks may not work correctly, see this comment on #179 for a workaround using ffmpeg or mkvmerge
  • +
+

0.5.2 (March 29, 2020)

+
    +
  • [enhancement] --min-duration now accepts a timecode in addition to frame number (#128, thanks @tonycpsu)
  • +
  • [feature] Add --image-frame-margin option to save-images command to ignore a number of frames at the start/end of a scene (#129, thanks @tonycpsu)
  • +
  • [bugfix] --min-scene-len option was not respected by first scene (#105, thanks @charlesvestal)
  • +
  • [bugfix] Splitting videos with an analyzed duration only splits within analyzed area (#106, thanks @charlesvestal)
  • +
  • [bugfix] Improper start timecode applied to the split-video command when using ffmpeg (#93, thanks @typoman)
  • +
  • [bugfix] Added links and filename sanitation to html output (#139 and #140, thanks @wjs018)
  • +
  • [bugfix] UnboundLocalError in detect_scenes when frame_skip is larger than 0 (#126, thanks @twostarxx)
  • +
+

0.5.1.1 (August 3, 2019)

+
    +
  • minor re-release of v0.5.1 which updates the setup.py file to return OpenCV as an optional dependency
  • +
  • to install from pip now with all dependencies: pip install scenedetect[opencv,progress_bar]
  • +
  • to install only PySceneDetect: pip install scenedetect (separate OpenCV installation required)
  • +
  • the release notes of v0.5.1 have been modified to include the prior command
  • +
  • no change to PySceneDetect program version
  • +
  • [feature] add get_duration method to VideoManager (#109, thanks @arianaa30)
  • +
+

0.5.1 (July 20, 2019)

+
    +
  • [feature] Add new export-html command to the CLI (thanks @wjs018)
  • +
  • [bugfix] VideoManager read function failed on multiple videos (thanks @ivan23kor)
  • +
  • [bugfix] Fix crash when no scenes are detected (#79, thanks @raj6996)
  • +
  • [bugfix] Fixed OpenCV not getting installed due to missing dependency (#73)
  • +
  • [enhance] When no scenes are detected, the whole video is now returned instead of nothing (thanks @piercus)
  • +
  • Removed Windows installer due to binary packages now being available, and to streamline the release process (see #102 for more information). When you type pip install scenedetect[opencv,progress_bar], all dependencies will be installed.
  • +
+

0.5 (August 31, 2018)

+
    +
  • major release, includes stable Python API with examples and updated documentation
  • +
  • numerous changes to command-line interface with addition of sub-commands (see the new manual for updated usage information)
  • +
  • [feature] videos are now split using ffmpeg by default, resulting in frame-perfect cuts (can still use mkvmerge by specifying the -c/--copy argument to the split-video command)
  • +
  • [enhance] image filename numbers are now consistent with those of split video scenes (PR #39, thanks @e271828-)
  • +
  • [enhance] 5-10% improvement in processing performance due to reduced memory copy operations (PR #40, thanks @elcombato)
  • +
  • [enhance] updated exception handling to raise proper standard exceptions (PR #37, thanks @talkain)
  • +
  • several fixes to the documentation, including improper dates and outdated CLI arguments (PR #26 and #, thanks @elcombato, and @colelawrence)
  • +
  • numerous other PRs and issues/bug reports that have been fixed - there are too many to list individually here, so I want to extend a big thank you to everyone who contributed to making this release better
  • +
  • [enhance] add Sphinx-generated API documentation (available at: http://manual.scenedetect.com)
  • +
  • [project] move from BSD 2-clause to 3-clause license
  • +
+
+

PySceneDetect 0.4

+

0.4 (January 14, 2017)

+
    +
  • major release, includes integrated scene splitting via mkvmerge, changes meaning of -o / --output option
  • +
  • [feature] specifying -o OUTPUT_FILE.mkv will now automatically split the input video, generating a new video clip for each detected scene in sequence, starting with OUTPUT_FILE-001.mkv
  • +
  • [enhance] CSV file output is now specified with the -co / --csv-output option (note, used to be -o in versions of PySceneDetect < 0.4)
  • +
+
+

PySceneDetect 0.3-beta

+

0.3.6 (January 12, 2017)

+
    +
  • [enhance] performance improvement when using --frameskip option (thanks @marcelluzs)
  • +
  • [internal] moved application state and shared objects to a consistent interface (the SceneManager object) to greatly reduce the number of required arguments for certain API functions
  • +
  • [enhance] added installer for Windows builds (64-bit only currently)
  • +
+

0.3.5 (August 2, 2016)

+
    +
  • [enhance] initial release of portable build for Windows (64-bit only), including all dependencies
  • +
  • [bugfix] fix unrelated exception thrown when video could not be loaded (thanks @marcelluzs)
  • +
  • [internal] fix variable name typo in API documentation
  • +
+

0.3.4 (February 8, 2016)

+
    +
  • [enhance] add scene length, in seconds, to output file (-o) for easier integration with ffmpeg/libav
  • +
  • [enhance] improved performance of content detection mode by caching intermediate HSV frames in memory (approx. 2x faster)
  • +
  • [enhance] show timecode values in terminal when using extended output (-l)
  • +
  • [feature] add fade bias option (-fb / --fade-bias) to command line (threshold mode only)
  • +
+

0.3.3 (January 27, 2016)

+
    +
  • [bugfix] output scenes are now correctly written to specified output file when using -o flag (fixes #11)
  • +
  • [bugfix] fix indexing exception when using multiple scene detectors and outputting statistics
  • +
  • [internal] distribute package on PyPI, version move from beta to stable
  • +
  • [internal] add function to convert frame number to formatted timecode
  • +
  • [internal] move file and statistic output to Python csv module
  • +
+

0.3.2-beta (January 26, 2016)

+
    +
  • [feature] added -si / --save-images flag to enable saving the first and last frames of each detected scene as an image, saved in the current working directory with the original video filename as the output prefix
  • +
  • [feature] added command line options for setting start and end times for processing (-st and -et)
  • +
  • [feature] added command line option to specify maximum duration to process (-dt, overrides -et)
  • +
+

0.3.1-beta (January 23, 2016)

+
    +
  • [feature] added downscaling/subsampling option (-df / --downscale-factor) to improve performance on higher resolution videos
  • +
  • [feature] added frameskip option (-fs / --frame-skip) to improve performance on high framerate videos, at expense of frame accuracy and possible inaccurate scene cut prediction
  • +
  • [enhance] added setup.py to allow for one-line installation (just run python setup.py install after downloading and extracting PySceneDetect)
  • +
  • [internal] additional API functions to remove requirement on passing OpenCV video objects, and allow just a file path instead
  • +
+

0.3-beta (January 8, 2016)

+
    +
  • major release, includes improved detection algorithms and complete internal code refactor
  • +
  • [feature] content-aware scene detection using HSV-colourspace based algorithm (use -d content)
  • +
  • [enhance] added CLI flags to allow user changes to more algorithm properties
  • +
  • [internal] re-implemented threshold-based scene detection algorithm under new interface
  • +
  • [internal] major code refactor including standard detection algorithm interface and API
  • +
  • [internal] remove statistics mode until update to new detection mode interface
  • +
+
+

PySceneDetect 0.2-alpha

+

0.2.4-alpha (December 22, 2015)

+
    +
  • [bugfix] updated OpenCV compatibility with self-reported version on some Linux distributions
  • +
+

0.2.3-alpha (August 7, 2015)

+
    +
  • [bugfix] updated PySceneDetect to work with latest OpenCV module (ver > 3.0)
  • +
  • [bugfix] added compatibility/legacy code for older versions of OpenCV
  • +
  • [feature] statsfile generation includes expanded frame metrics
  • +
+

0.2.2-alpha (November 25, 2014)

+
    +
  • [feature] added statistics mode for generating frame-by-frame analysis (-s / --statsfile flag)
  • +
  • [bugfix] fixed improper timecode conversion
  • +
+

0.2.1-alpha (November 16, 2014)

+
    +
  • [enhance] proper timecode format (HH:MM:SS.nnnnn)
  • +
  • [enhance] one-line of CSV timecodes added for easy splitting with external tool
  • +
+

0.2-alpha (June 9, 2014)

+
    +
  • [enhance] now provides discrete scene list (in addition to fades)
  • +
  • [feature] ability to output to file (-o / --output flag)
  • +
+
+

PySceneDetect 0.1-alpha

+

0.1-alpha (June 8, 2014)

+
    +
  • first public release
  • +
  • [feature] threshold-based fade in/out detection
  • +
+
+

Development

+

PySceneDetect 0.7.2 (TBD)

+
    +
  • [general] The scenedetect-core package introduced in 0.7.1 has been discontinued, and its only release (0.7.1) yanked from PyPI: pip cannot safely support multiple packages that install the same module files, and restructuring the existing packages around a shared core would break in-place upgrades. Existing scenedetect-core installs keep working but will not receive updates; continue to install scenedetect or scenedetect-headless as usual.
  • +
  • [improvement] HistogramDetector (detect-hist) default threshold changed from 0.05 to 0.20 and default bins from 256 to 128, calibrated from the benchmark sweep for significantly better accuracy. Default output for this detector will change #559
  • +
  • [improvement] HashDetector (detect-hash) default threshold changed from 0.395 to 0.35 and default size from 16 to 8, calibrated from the benchmark sweep for better accuracy. Default output for this detector will change, including the statsfile metric key (now hash_dist [size=8 lowpass=2]) #559
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + + diff --git a/cli/index.html b/cli/index.html new file mode 100644 index 00000000..e4548d23 --- /dev/null +++ b/cli/index.html @@ -0,0 +1,444 @@ + + + + + + + + Command-Line - PySceneDetect + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+
+
+ +

PySceneDetect CLI

+

See the documentation for a complete reference to the scenedetect command with more examples.

+

Quickstart

+

Split input video on each fast cut using ffmpeg:

+
scenedetect -i video.mp4 split-video
+
+

Save some frames from each cut:

+
scenedetect -i video.mp4 save-images
+
+

Skip the first 10 seconds of the input video:

+
scenedetect -i video.mp4 time -s 10s
+
+

Example

+

As a concrete example to become familiar with PySceneDetect, let's use the following short clip from the James Bond movie, GoldenEye (Copyright © 1995 MGM):

+

https://www.youtube.com/watch?v=OMgIPnCnlbQ

+

You can download the clip from here (right-click and save the video in your working directory as goldeneye.mp4).

+

Let's split this scene into clips on each fast cut. This means we need to use content-aware detection mode (detect-content) or adaptive mode (detect-adaptive). If the video instead contains fade-in/fade-out transitions you want to find, you can use detect-threshold instead. If no detector is specified, detect-adaptive will be used by default.

+

Let's first save a scene list in CSV format and generate some images of each scene to check the output:

+
scenedetect --input goldeneye.mp4 detect-adaptive list-scenes save-images
+
+

Running the above command, in the working directory, you should see a file goldeneye-Scenes.csv, as well as individual frames for the start/middle/end of each scene starting with goldeneye-Scene-001-01.jpg. The results should appear as follows:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Scene #Start TimePreview
100:00:00.000
200:00:03.754
300:00:08.759
400:00:10.802
500:00:15.599
600:00:27.110
700:00:34.117
800:00:36.536
.........
1800:01:06.316
1900:01:10.779
2000:01:18.036
2100:01:19.913
2200:01:21.999
+

Splitting Video into Clips

+

The split-video command can be used to automatically split the input video using ffmpeg or mkvmerge. For example:

+
scenedetect -i goldeneye.mp4 split-video
+
+

Type scenedetect split-video --help for a full list of options which can be specified for video splitting, including high quality mode (-hq/--high-quality) or copy mode (-c/--copy).

+

You can also specify -h / --high-quality to produces near lossless results, or -p/--preset and -crf/--rate-factor (call scenedetect help split-video for details). If either -c/--copy or -m/--mkvmerge is specified, codec copying mode is used, at the expense of frame accurate cuts. Optionally, you can also specify the x264 -p/--preset and -crf/--rate-factor (see scenedetect split-video --help for details).

+

Detection Methods

+

PySceneDetect can look for fades in/out using detect-threshold (comparing each frame to a set black level) or find fast cuts using detect-content (compares each frame looking for changes in content). There also is detect-adaptive, which uses the same scoring as detect-content, but compares the ratio of each frame score to its neighbors.

+

Each mode has slightly different parameters, and is described in detail below. Most detector parameters can also be set with a config file.

+

In general, use detect-threshold mode if you want to detect scene boundaries using fades/cuts in/out to black. If the video uses a lot of fast cuts between content, and has no well-defined scene boundaries, you should use the detect-adaptive or detect-content modes. Once you know what detection mode to use, you can try the parameters recommended below, or generate a statistics file (using the -s / --stats flag) in order to determine the correct parameters - specifically, the proper threshold value.

+

Content-Aware Detection

+

Unlike threshold mode, content-aware mode looks at the difference between each pair of adjacent frames, triggering a scene break when this difference exceeds the threshold value.

+

The optimal threshold can be determined by generating a stats file (-s), opening it with a spreadsheet editor (e.g. Excel), and examining the content_val column (example). This value should be very small between similar frames, and grow large when a big change in content is noticed (look at the values near frame numbers/times where you know a scene change occurs). The threshold value should be set so that most scenes fall below the threshold value, and scenes where changes occur should exceed the threshold value (thus triggering a scene change).

+

Threshold Detection

+

Threshold-based mode is what most traditional scene detection programs use, which looks at the average intensity of the current frame, triggering a scene break when the intensity falls below the threshold (or crosses back upwards). The default threshold when using the detect-threshold is 12 (e.g. detect-threshold is the same as detect-threshold --threshold 12 when the -t / --threshold option is not supplied), which is a good value to try when detecting fade outs to black on most videos.

+
scenedetect -i my_video.mp4 -s my_video.stats.mp4 detect-threshold
+
+
scenedetect -i my_video.mp4 -s my_video.stats.mp4 detect-threshold -t 20
+
+

Using values for threshold less than 8 may cause problems with some videos, especially those encoded at lower bitrates or with limited dynamic range.

+

The optimal threshold can be determined by generating a statsfile (-s), opening it with a spreadsheet editor (e.g. Excel), and examining the delta_rgb column. These values represent the average intensity of the pixels for that particular frame (taken by averaging the R, G, and B values over the whole frame). The threshold value should be set so that the average intensity of most frames in content scenes lie above the threshold value, and scenes where scene changes/breaks occur should fall under the threshold value (thus triggering a scene change).

+

Adaptive Detection

+

The detect-adaptive mode compares each frame's score as calculated by detect-content with its neighbors. This score is what forms the adaptive_ratio metric in the statsfile. You can also configure the amount of neighboring frames via the frame-window option, as well as the minimum change in content_val score using min-content-val.

+

Detection Parameters

+

Detectors take a variety of parameters, which can be configured via command-line or by using a config file. If the default parameters do not produce correct results, you can generate a stats file using the -s / --stats option.

+

For example, with detect-content, if the default threshold of 27 does not produce correct results, we can determine the proper threshold by first generating a stats file:

+
scenedetect --input goldeneye.mp4 --stats goldeneye.stats.csv detect-adaptive
+
+

We can then plot the values of the content_val column:

+

goldeneye.mp4 statistics graph

+

The peaks in values correspond to the scene breaks in the input video. In some cases the threshold may need to be raised or lowered accordingly.

+

Saving Image Previews of Detected Scenes

+

PySceneDetect can automatically save the beginning and ending frame of each detected scene by using the save-images command. If present, the first and last frames of each scene will be saved in the current working directory, using the filename of the input video.

+

Files marked 00 represent the starting frame of the scene, and those marked 01 represent the last frame (e.g. testvideo.mp4.Scene-4-01.jpg). By default, two images are generated.

+

Coming soon: If more are specified via the -n flag, they will start from 00 (the first frame) and be evenly spaced throughout the scene until the last frame, which will be numbered N-1.

+

Improving Processing Speed/Performance

+

The following arguments are global program options, and need to be applied before any commands (e.g. detect-content, list-scenes). They can be used to achieve performance gains for some source material with a variable loss of accuracy.

+

Assuming the input video is of a high enough resolution, a significant performance gain can be achieved by sub-sampling (down-scaling) the input image by a specific integer factor (2x, 3x, 4x, 5x...). This is applied automatically to some degree based on the input video size, but can be overridden manually with the -d / --downscale option.

+

This factor represents how many pixels are "skipped" in both the x- and y- directions, effectively down-scaling the image (using nearest-neighbor sampling) by the factor specified (the new resolution being W/factor x H/factor if the old resolution is W x H).

+

Another method that can be used to gain a performance boost is frame skipping. This method, however, severely reduces frame-accurate scene cuts, so it should only be used with high FPS material (ideally > 60 FPS), at low values (try not to exceed a value of 1 or 2 if using -fs / --frame-skip), in cases where this is acceptable. Using the frame skip option also disallows the use of a stats file, which offsets the speed gain if the same video needs to be processed multiple times (e.g. to determine the optimal threshold).

+

The option still remains, however, for the set of cases where it is still required. For example, if we skip every other frame (e.g. using --frame-skip 1), the processing speed should roughly double.

+

If set too large, enough frames may be skipped each time that the threshold is met during every iteration, continually triggering scene changes. This is because frame skipping essentially raises the threshold between frames in the same scene (making them more likely to appear as cuts) while not affecting the threshold between frames of different scenes.

+

This makes the two harder to distinguish, and can cause additional false scene cuts to be detected. While this can be compensated for by raising the threshold value, this increases the probability of missing a real/true scene cut - thus, the use of the -fs / --frame-skip option is discouraged.

+

Seeking, Duration, and Setting Start / Stop Times

+

Specifying the time command allows control over what portion of the video PySceneDetect processes. The time command accepts three options: start time (-s / -start), end time (-e / -end), and duration (-d / --duration). Specifying both end time and duration is redundant, and in this case, duration overrides end time. Timecodes can be given in seconds (100.0), frames (no decimal place, 100), or timecode as HH:MM:SS[.nnn] (12:34:56.789).

+

For example, let's say we have a video shot at 30 FPS, and want to analyze only the segment from the 5 to the 6.5 minute mark in the video (we want to analyze the 90 seconds [2700 frames] between 00:05:00 and 00:06:30). The following commands are all thus equivalent in this regard (assuming we are using the content detector):

+
scenedetect -i my_video.mp4 time --start 00:05:00 --end 00:06:30
+
+
scenedetect -i my_video.mp4 time --start 300s --end 390s
+
+
scenedetect -i my_video.mp4 time --start 300s --duration 90s
+
+
scenedetect -i my_video.mp4 time --start 300s --duration 2700
+
+

This demonstrates the different timecode formats, interchanging end time with duration and vice-versa, and precedence of setting duration over end time.

+

Config File

+

A configuration file path can be specified using the -c/--config argument. PySceneDetect also looks for a config file named scenedetect.cfg in one of the following locations:

+
    +
  • +

    Windows:

    +
      +
    • C:/Users/%USERNAME%/AppData/Local/PySceneDetect/scenedetect.cfg
    • +
    +
  • +
  • +

    Linux:

    +
      +
    • ~/.config/PySceneDetect/scenedetect.cfg
    • +
    • $XDG_CONFIG_HOME/scenedetect.cfg
    • +
    +
  • +
  • +

    Mac:

    +
      +
    • ~/Library/Preferences/PySceneDetect/scenedetect.cfg
    • +
    +
  • +
+

Run scenedetect --help to see the exact path on your system which will be used (it will be listed under the help text for the -c/--config option). You can click here to download a scenedetect.cfg config file to use as a template. Note that lines starting with a # are comments and will be ignored. The scenedetect.cfg template file is also available in the folder where PySceneDetect is installed.

+

Specifying a config file path using -c/--config overrides the user config file. Specifying values on the command line will override those values in the config file.

+

The syntax of a configuration file is:

+
[command]
+option_a = value
+#comment
+option_b = 1
+
+

Example

+
[global]
+default-detector = detect-content
+min-scene-len = 0.8s
+
+[detect-content]
+threshold = 32
+weights = 1.0 0.5 1.0 0.2
+
+[split-video]
+preset = slow
+rate-factor = 17
+# Don't need to use quotes even if filename contains spaces
+filename = $VIDEO_NAME-Clip-$SCENE_NUMBER
+
+[save-images]
+format = jpeg
+quality = 80
+num-images = 3
+
+

See the scenedetect.cfg file in the location you installed PySceneDetect or download it from Github for a complete listing of all configuration options.

+

  Video Splitting Requirements

+

PySceneDetect can use either ffmpeg or mkvmerge to split videos automatically.

+

By default, when specifying the split-video command, ffmpeg will be used to split the video. If the -c/--copy option is also set (e.g. split-video --copy), mkvmerge will be used to split the video instead.

+

FFmpeg

+

You can download ffmpeg from: https://ffmpeg.org/download.html

+

Note that Linux users should use a package manager (e.g. sudo apt-get install ffmpeg). Windows users may require additional steps in order for PySceneDetect to detect ffmpeg - see the section Manually Enabling split-video Support below for details.

+

mkvmerge

+

You can download and install mkvmerge as part of the mkvtoolnix package from: +https://mkvtoolnix.download/downloads.html

+

Note that Windows users should use the installer/setup, and Linux users should use their system package manager, otherwise PySceneDetect may not be able to find mkvmerge. If this is the case, see the section below to enable support for the split-video --copy command manually.

+

Enabling split-video Support

+

If PySceneDetect cannot find the respective tool installed on your system, you have three options:

+
    +
  1. +

    Place the tool in the same location that PySceneDetect is installed (e.g. copy and paste mkvmerge.exe into the same place scenedetect.exe is located). This is the easiest solution for most users.

    +
  2. +
  3. +

    Add the directory where you installed ffmpeg/mkvmerge to your system's PATH environment variable, ensuring that you can use the ffmpeg/mkvmerge command from any terminal/command prompt. This is the best solution for advanced users.

    +
  4. +
  5. +

    Place the tool in a location already in your system's PATH variable (e.g. C:/Windows). This is not recommended, but may be the only solution on systems without administrative rights.

    +
  6. +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + + diff --git a/contributing/index.html b/contributing/index.html new file mode 100644 index 00000000..79dc7dad --- /dev/null +++ b/contributing/index.html @@ -0,0 +1,199 @@ + + + + + + + + Bugs & Contributing - PySceneDetect + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+
+
+ +

  Bug Reports

+

Bugs, issues, features, and improvements to PySceneDetect are handled through the issue tracker on Github. If you run into any bugs using PySceneDetect, please create a new issue.

+

Try to find an existing issue before creating a new one, as there may be a workaround posted there. Additional information is also helpful for existing reports.

+

  Contributing to Development

+

Development of PySceneDetect happens on github.com/Breakthrough/PySceneDetect. Pull requests are accepted and encouraged. Where possible, PRs should be submitted with a dedicated entry in the issue tracker. Issues and features are typically grouped into version milestones.

+

The following checklist covers the basics of pre-submission requirements:

+
    +
  • Code passes all unit tests (run pytest)
  • +
  • Code passes static analysis and formatting checks (ruff check and ruff format)
  • +
  • Follows the Google Python Style Guide
  • +
+

Note that PySceneDetect is released under the BSD 3-Clause license, and submitted code should comply with this license (see License & Copyright Information for details).

+

  Features That Need Help

+

The following is a "wishlist" of features which PySceneDetect eventually should have, but does not currently due to lack of resources. Anyone who is able to contribute in any capacity to these items is encouraged to do so by starting a dialogue by opening a new issue on Github as per above.

+

Flash Suppression

+

Some detection methods struggle with bright flashes and fast camera movement. The detection pipeline has some filters in place to deal with these cases, but there are still drawbacks. We are actively seeking methods which can improve both performance and accuracy in these cases.

+

Automatic Thresholding

+

The detect-content command requires a manual threshold to be set currently. Methods to use peak detection to dynamically determine when scene cuts occur would allow for the program to work with a much wider amount of material without requiring manual tuning, but would require statistical analysis.

+

Ideally, this would be something like -threshold=auto as a default.

+

Dissolve Detection

+

Depending on the length of the dissolve and parameters being used, detection accuracy for these types of cuts can vary widely. A method to improve accuracy with minimal performance loss is an open problem.

+

Advanced Strategies

+

Research into detection methods and performance are ongoing. All contributions in this regard are most welcome.

+

GUI

+

A graphical user interface will be crucial for making PySceneDetect approachable by a wider audience. There have been several suggested designs, but nothing concrete has been developed yet. Any proposed solution for the GUI should work across Windows, Linux, and OSX.

+

Localization

+

PySceneDetect currently is not localized for other languages. Anyone who can help improve how localization can be approached for development material is encouraged to contribute in any way possible. Whether it is the GUI program, the command line interface, or documentation, localization will allow PySceneDetect to be used by much more users in their native languages.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + + diff --git a/copyright/index.html b/copyright/index.html new file mode 100644 index 00000000..94741d73 --- /dev/null +++ b/copyright/index.html @@ -0,0 +1,245 @@ + + + + + + + + License & Copyright - PySceneDetect + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+
+
+ +

PySceneDetect License Agreement

+

+                PySceneDetect License (BSD 3-Clause)
+          < http://www.bcastell.com/projects/PySceneDetect >
+
+Copyright (C) 2014, Brandon Castellano.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+    1. Redistributions of source code must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above
+       copyright notice, this list of conditions and the following
+       disclaimer in the documentation and/or other materials
+       provided with the distribution.
+
+    3. Neither the name of the copyright holder nor the names of its
+       contributors may be used to endorse or promote products derived
+       from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+

Ancillary Software Licenses

+

This section contains links to the license agreements for all third-party software libraries used and distributed with PySceneDetect. You can find copies of all the relevant license agreements referenced below if you installed a copy of PySceneDetect by looking at the LICENSE files in the installation directory.

+
+

click

+ +

NumPy

+ +

OpenCV

+ +

PyAV

+ +

simpletable

+ +

tqdm

+ +
+

FFmpeg and mkvmerge

+

This software may also invoke mkvmerge or FFmpeg, if available.

+

FFmpeg is a trademark of Fabrice Bellard. +mkvmerge is Copyright (C) 2005-2016, Matroska.

+

Certain distributions of PySceneDetect may include ffmpeg. See the +thirdparty/LICENSE-FFMPEG file or visit [ https://ffmpeg.org ].

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + +
+ + + + + + + + + + diff --git a/css/fonts/Roboto-Slab-Bold.woff b/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 00000000..6cb60000 Binary files /dev/null and b/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/css/fonts/Roboto-Slab-Bold.woff2 b/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 00000000..7059e231 Binary files /dev/null and b/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/css/fonts/Roboto-Slab-Regular.woff b/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 00000000..f815f63f Binary files /dev/null and b/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/css/fonts/Roboto-Slab-Regular.woff2 b/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 00000000..f2c76e5b Binary files /dev/null and b/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/css/fonts/fontawesome-webfont.eot b/css/fonts/fontawesome-webfont.eot new file mode 100644 index 00000000..e9f60ca9 Binary files /dev/null and b/css/fonts/fontawesome-webfont.eot differ diff --git a/css/fonts/fontawesome-webfont.svg b/css/fonts/fontawesome-webfont.svg new file mode 100644 index 00000000..855c845e --- /dev/null +++ b/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/fonts/fontawesome-webfont.ttf b/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..35acda2f Binary files /dev/null and b/css/fonts/fontawesome-webfont.ttf differ diff --git a/css/fonts/fontawesome-webfont.woff b/css/fonts/fontawesome-webfont.woff new file mode 100644 index 00000000..400014a4 Binary files /dev/null and b/css/fonts/fontawesome-webfont.woff differ diff --git a/css/fonts/fontawesome-webfont.woff2 b/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 00000000..4d13fc60 Binary files /dev/null and b/css/fonts/fontawesome-webfont.woff2 differ diff --git a/css/fonts/lato-bold-italic.woff b/css/fonts/lato-bold-italic.woff new file mode 100644 index 00000000..88ad05b9 Binary files /dev/null and b/css/fonts/lato-bold-italic.woff differ diff --git a/css/fonts/lato-bold-italic.woff2 b/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 00000000..c4e3d804 Binary files /dev/null and b/css/fonts/lato-bold-italic.woff2 differ diff --git a/css/fonts/lato-bold.woff b/css/fonts/lato-bold.woff new file mode 100644 index 00000000..c6dff51f Binary files /dev/null and b/css/fonts/lato-bold.woff differ diff --git a/css/fonts/lato-bold.woff2 b/css/fonts/lato-bold.woff2 new file mode 100644 index 00000000..bb195043 Binary files /dev/null and b/css/fonts/lato-bold.woff2 differ diff --git a/css/fonts/lato-normal-italic.woff b/css/fonts/lato-normal-italic.woff new file mode 100644 index 00000000..76114bc0 Binary files /dev/null and b/css/fonts/lato-normal-italic.woff differ diff --git a/css/fonts/lato-normal-italic.woff2 b/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 00000000..3404f37e Binary files /dev/null and b/css/fonts/lato-normal-italic.woff2 differ diff --git a/css/fonts/lato-normal.woff b/css/fonts/lato-normal.woff new file mode 100644 index 00000000..ae1307ff Binary files /dev/null and b/css/fonts/lato-normal.woff differ diff --git a/css/fonts/lato-normal.woff2 b/css/fonts/lato-normal.woff2 new file mode 100644 index 00000000..3bf98433 Binary files /dev/null and b/css/fonts/lato-normal.woff2 differ diff --git a/css/theme.css b/css/theme.css new file mode 100644 index 00000000..ad773009 --- /dev/null +++ b/css/theme.css @@ -0,0 +1,13 @@ +/* + * This file is copied from the upstream ReadTheDocs Sphinx + * theme. To aid upgradability this file should *not* be edited. + * modifications we need should be included in theme_extra.css. + * + * https://github.com/readthedocs/sphinx_rtd_theme + */ + + /* sphinx_rtd_theme version 1.2.0 | MIT license */ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} diff --git a/css/theme_extra.css b/css/theme_extra.css new file mode 100644 index 00000000..9f4b063c --- /dev/null +++ b/css/theme_extra.css @@ -0,0 +1,191 @@ +/* + * Wrap inline code samples otherwise they shoot of the side and + * can't be read at all. + * + * https://github.com/mkdocs/mkdocs/issues/313 + * https://github.com/mkdocs/mkdocs/issues/233 + * https://github.com/mkdocs/mkdocs/issues/834 + */ +.rst-content code { + white-space: pre-wrap; + word-wrap: break-word; + padding: 2px 5px; +} + +/** + * Make code blocks display as blocks and give them the appropriate + * font size and padding. + * + * https://github.com/mkdocs/mkdocs/issues/855 + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/issues/233 + */ +.rst-content pre code { + white-space: pre; + word-wrap: normal; + display: block; + padding: 12px; + font-size: 12px; +} + +/** + * Fix code colors + * + * https://github.com/mkdocs/mkdocs/issues/2027 + */ +.rst-content code { + color: #E74C3C; +} + +.rst-content pre code { + color: #000; + background: #f8f8f8; +} + +/* + * Fix link colors when the link text is inline code. + * + * https://github.com/mkdocs/mkdocs/issues/718 + */ +a code { + color: #2980B9; +} +a:hover code { + color: #3091d1; +} +a:visited code { + color: #9B59B6; +} + +/* + * The CSS classes from highlight.js seem to clash with the + * ReadTheDocs theme causing some code to be incorrectly made + * bold and italic. + * + * https://github.com/mkdocs/mkdocs/issues/411 + */ +pre .cs, pre .c { + font-weight: inherit; + font-style: inherit; +} + +/* + * Fix some issues with the theme and non-highlighted code + * samples. Without and highlighting styles attached the + * formatting is broken. + * + * https://github.com/mkdocs/mkdocs/issues/319 + */ +.rst-content .no-highlight { + display: block; + padding: 0.5em; + color: #333; +} + + +/* + * Additions specific to the search functionality provided by MkDocs + */ + +.search-results { + margin-top: 23px; +} + +.search-results article { + border-top: 1px solid #E1E4E5; + padding-top: 24px; +} + +.search-results article:first-child { + border-top: none; +} + +form .search-query { + width: 100%; + border-radius: 50px; + padding: 6px 12px; /* csslint allow: box-model */ + border-color: #D1D4D5; +} + +/* + * Improve inline code blocks within admonitions. + * + * https://github.com/mkdocs/mkdocs/issues/656 + */ + .rst-content .admonition code { + color: #404040; + border: 1px solid #c7c9cb; + border: 1px solid rgba(0, 0, 0, 0.2); + background: #f8fbfd; + background: rgba(255, 255, 255, 0.7); +} + +/* + * Account for wide tables which go off the side. + * Override borders to avoid weirdness on narrow tables. + * + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/pull/1034 + */ +.rst-content .section .docutils { + width: 100%; + overflow: auto; + display: block; + border: none; +} + +td, th { + border: 1px solid #e1e4e5 !important; /* csslint allow: important */ + border-collapse: collapse; +} + +/* + * Without the following amendments, the navigation in the theme will be + * slightly cut off. This is due to the fact that the .wy-nav-side has a + * padding-bottom of 2em, which must not necessarily align with the font-size of + * 90 % on the .rst-current-version container, combined with the padding of 12px + * above and below. These amendments fix this in two steps: First, make sure the + * .rst-current-version container has a fixed height of 40px, achieved using + * line-height, and then applying a padding-bottom of 40px to this container. In + * a second step, the items within that container are re-aligned using flexbox. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ + .wy-nav-side { + padding-bottom: 40px; +} + +/* + * The second step of above amendment: Here we make sure the items are aligned + * correctly within the .rst-current-version container. Using flexbox, we + * achieve it in such a way that it will look like the following: + * + * [No repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * [With repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-versions .rst-current-version { + padding: 0 12px; + display: flex; + font-size: initial; + justify-content: space-between; + align-items: center; + line-height: 40px; +} + +/* + * Please note that this amendment also involves removing certain inline-styles + * from the file ./mkdocs/themes/readthedocs/versions.html. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-current-version span { + flex: 1; + text-align: center; +} diff --git a/dist/logo/pyscenedetect-logo-darkmode.svg b/dist/logo/pyscenedetect-logo-darkmode.svg deleted file mode 100644 index 333ee5a1..00000000 --- a/dist/logo/pyscenedetect-logo-darkmode.svg +++ /dev/null @@ -1,247 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - PySceneDetect - - - diff --git a/docs/.buildinfo b/docs/.buildinfo new file mode 100644 index 00000000..cd12827e --- /dev/null +++ b/docs/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: ca33adaa2a337c7d23dacd900cf35397 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/.doctrees/api.doctree b/docs/.doctrees/api.doctree new file mode 100644 index 00000000..86d24c81 Binary files /dev/null and b/docs/.doctrees/api.doctree differ diff --git a/docs/.doctrees/api/backends.doctree b/docs/.doctrees/api/backends.doctree new file mode 100644 index 00000000..b9346460 Binary files /dev/null and b/docs/.doctrees/api/backends.doctree differ diff --git a/docs/.doctrees/api/detectors.doctree b/docs/.doctrees/api/detectors.doctree new file mode 100644 index 00000000..8b01cd4e Binary files /dev/null and b/docs/.doctrees/api/detectors.doctree differ diff --git a/docs/.doctrees/api/frame_timecode.doctree b/docs/.doctrees/api/frame_timecode.doctree new file mode 100644 index 00000000..931acb9b Binary files /dev/null and b/docs/.doctrees/api/frame_timecode.doctree differ diff --git a/docs/.doctrees/api/migration_guide.doctree b/docs/.doctrees/api/migration_guide.doctree new file mode 100644 index 00000000..ac3a5161 Binary files /dev/null and b/docs/.doctrees/api/migration_guide.doctree differ diff --git a/docs/.doctrees/api/platform.doctree b/docs/.doctrees/api/platform.doctree new file mode 100644 index 00000000..518c83ae Binary files /dev/null and b/docs/.doctrees/api/platform.doctree differ diff --git a/docs/.doctrees/api/scene_detector.doctree b/docs/.doctrees/api/scene_detector.doctree new file mode 100644 index 00000000..657cb40d Binary files /dev/null and b/docs/.doctrees/api/scene_detector.doctree differ diff --git a/docs/.doctrees/api/scene_manager.doctree b/docs/.doctrees/api/scene_manager.doctree new file mode 100644 index 00000000..d733917c Binary files /dev/null and b/docs/.doctrees/api/scene_manager.doctree differ diff --git a/docs/.doctrees/api/stats_manager.doctree b/docs/.doctrees/api/stats_manager.doctree new file mode 100644 index 00000000..faa153ef Binary files /dev/null and b/docs/.doctrees/api/stats_manager.doctree differ diff --git a/docs/.doctrees/api/video_splitter.doctree b/docs/.doctrees/api/video_splitter.doctree new file mode 100644 index 00000000..9756a1cf Binary files /dev/null and b/docs/.doctrees/api/video_splitter.doctree differ diff --git a/docs/.doctrees/api/video_stream.doctree b/docs/.doctrees/api/video_stream.doctree new file mode 100644 index 00000000..2297da64 Binary files /dev/null and b/docs/.doctrees/api/video_stream.doctree differ diff --git a/docs/.doctrees/cli.doctree b/docs/.doctrees/cli.doctree new file mode 100644 index 00000000..28097c0f Binary files /dev/null and b/docs/.doctrees/cli.doctree differ diff --git a/docs/.doctrees/cli/backends.doctree b/docs/.doctrees/cli/backends.doctree new file mode 100644 index 00000000..b6304647 Binary files /dev/null and b/docs/.doctrees/cli/backends.doctree differ diff --git a/docs/.doctrees/cli/config_file.doctree b/docs/.doctrees/cli/config_file.doctree new file mode 100644 index 00000000..3b491e56 Binary files /dev/null and b/docs/.doctrees/cli/config_file.doctree differ diff --git a/docs/.doctrees/environment.pickle b/docs/.doctrees/environment.pickle new file mode 100644 index 00000000..58d0f01f Binary files /dev/null and b/docs/.doctrees/environment.pickle differ diff --git a/docs/.doctrees/index.doctree b/docs/.doctrees/index.doctree new file mode 100644 index 00000000..f3658c0f Binary files /dev/null and b/docs/.doctrees/index.doctree differ diff --git a/docs/0.6.1/.buildinfo b/docs/0.6.1/.buildinfo new file mode 100644 index 00000000..841e234a --- /dev/null +++ b/docs/0.6.1/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 0658a8343f19fbf0aa65fa5b78f9f4c8 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/0.6.1/.doctrees/api.doctree b/docs/0.6.1/.doctrees/api.doctree new file mode 100644 index 00000000..c6259422 Binary files /dev/null and b/docs/0.6.1/.doctrees/api.doctree differ diff --git a/docs/0.6.1/.doctrees/api/backends.doctree b/docs/0.6.1/.doctrees/api/backends.doctree new file mode 100644 index 00000000..d7183bad Binary files /dev/null and b/docs/0.6.1/.doctrees/api/backends.doctree differ diff --git a/docs/0.6.1/.doctrees/api/detectors.doctree b/docs/0.6.1/.doctrees/api/detectors.doctree new file mode 100644 index 00000000..eb6001c3 Binary files /dev/null and b/docs/0.6.1/.doctrees/api/detectors.doctree differ diff --git a/docs/0.6.1/.doctrees/api/frame_timecode.doctree b/docs/0.6.1/.doctrees/api/frame_timecode.doctree new file mode 100644 index 00000000..51582a35 Binary files /dev/null and b/docs/0.6.1/.doctrees/api/frame_timecode.doctree differ diff --git a/docs/0.6.1/.doctrees/api/migration_guide.doctree b/docs/0.6.1/.doctrees/api/migration_guide.doctree new file mode 100644 index 00000000..88ff8655 Binary files /dev/null and b/docs/0.6.1/.doctrees/api/migration_guide.doctree differ diff --git a/docs/0.6.1/.doctrees/api/platform.doctree b/docs/0.6.1/.doctrees/api/platform.doctree new file mode 100644 index 00000000..8b1aa7dc Binary files /dev/null and b/docs/0.6.1/.doctrees/api/platform.doctree differ diff --git a/docs/0.6.1/.doctrees/api/scene_detector.doctree b/docs/0.6.1/.doctrees/api/scene_detector.doctree new file mode 100644 index 00000000..c86d50fd Binary files /dev/null and b/docs/0.6.1/.doctrees/api/scene_detector.doctree differ diff --git a/docs/0.6.1/.doctrees/api/scene_manager.doctree b/docs/0.6.1/.doctrees/api/scene_manager.doctree new file mode 100644 index 00000000..d65e7fb7 Binary files /dev/null and b/docs/0.6.1/.doctrees/api/scene_manager.doctree differ diff --git a/docs/0.6.1/.doctrees/api/stats_manager.doctree b/docs/0.6.1/.doctrees/api/stats_manager.doctree new file mode 100644 index 00000000..51242d66 Binary files /dev/null and b/docs/0.6.1/.doctrees/api/stats_manager.doctree differ diff --git a/docs/0.6.1/.doctrees/api/video_splitter.doctree b/docs/0.6.1/.doctrees/api/video_splitter.doctree new file mode 100644 index 00000000..6cac0b98 Binary files /dev/null and b/docs/0.6.1/.doctrees/api/video_splitter.doctree differ diff --git a/docs/0.6.1/.doctrees/api/video_stream.doctree b/docs/0.6.1/.doctrees/api/video_stream.doctree new file mode 100644 index 00000000..487fbabf Binary files /dev/null and b/docs/0.6.1/.doctrees/api/video_stream.doctree differ diff --git a/docs/0.6.1/.doctrees/cli/backends.doctree b/docs/0.6.1/.doctrees/cli/backends.doctree new file mode 100644 index 00000000..743e44c4 Binary files /dev/null and b/docs/0.6.1/.doctrees/cli/backends.doctree differ diff --git a/docs/0.6.1/.doctrees/cli/commands.doctree b/docs/0.6.1/.doctrees/cli/commands.doctree new file mode 100644 index 00000000..16ed4fcb Binary files /dev/null and b/docs/0.6.1/.doctrees/cli/commands.doctree differ diff --git a/docs/0.6.1/.doctrees/cli/config_file.doctree b/docs/0.6.1/.doctrees/cli/config_file.doctree new file mode 100644 index 00000000..13f8d8dc Binary files /dev/null and b/docs/0.6.1/.doctrees/cli/config_file.doctree differ diff --git a/docs/0.6.1/.doctrees/cli/detectors.doctree b/docs/0.6.1/.doctrees/cli/detectors.doctree new file mode 100644 index 00000000..8f452135 Binary files /dev/null and b/docs/0.6.1/.doctrees/cli/detectors.doctree differ diff --git a/docs/0.6.1/.doctrees/cli/global_options.doctree b/docs/0.6.1/.doctrees/cli/global_options.doctree new file mode 100644 index 00000000..20924569 Binary files /dev/null and b/docs/0.6.1/.doctrees/cli/global_options.doctree differ diff --git a/docs/0.6.1/.doctrees/environment.pickle b/docs/0.6.1/.doctrees/environment.pickle new file mode 100644 index 00000000..38dc14ce Binary files /dev/null and b/docs/0.6.1/.doctrees/environment.pickle differ diff --git a/docs/0.6.1/.doctrees/index.doctree b/docs/0.6.1/.doctrees/index.doctree new file mode 100644 index 00000000..e47d4d2e Binary files /dev/null and b/docs/0.6.1/.doctrees/index.doctree differ diff --git a/docs/0.6.1/_sources/api.rst.txt b/docs/0.6.1/_sources/api.rst.txt new file mode 100644 index 00000000..23f2e210 --- /dev/null +++ b/docs/0.6.1/_sources/api.rst.txt @@ -0,0 +1,146 @@ + +*********************************************************************** +``scenedetect`` 🎬 Package +*********************************************************************** + +======================================================================= +Overview +======================================================================= + +The `scenedetect` API is designed to be extensible and easy to integrate with most application workflows. Many use cases are covered by the `Quickstart`_ and `Example`_ sections below. The `scenedetect` package provides: + + * :ref:`scenedetect.scene_manager 🎞️ `: The :py:class:`SceneManager ` class applies `SceneDetector` objects on video frames from a :ref:`VideoStream `. Also contains the :py:func:`save_images ` and :py:func:`write_scene_list ` / :py:func:`write_scene_list_html ` functions to export information about the detected scenes in various formats. + + * :ref:`scenedetect.detectors 🕵️ `: Scene/shot detection algorithms: + + * :py:mod:`ContentDetector `: detects fast changes/cuts in video content. + + * :py:mod:`ThresholdDetector `: detects changes in video brightness/intensity. + + * :py:mod:`AdaptiveDetector `: similar to `ContentDetector` but may result in less false negatives during rapid camera movement. + + * :ref:`scenedetect.video_stream 🎥 `: Contains :py:class:`VideoStream ` interface for video decoding using different backends (:py:mod:`scenedetect.backends`). Current supported backends: + + * OpenCV: :py:class:`VideoStreamCv2 ` + * PyAV: In Development + + * :ref:`scenedetect.video_splitter ✂️ `: Contains :py:func:`split_video_ffmpeg ` and :py:func:`split_video_mkvmerge ` to split a video based on the detected scenes. + + * :ref:`scenedetect.frame_timecode ⏱️ `: Contains + :py:class:`FrameTimecode ` + class for storing, converting, and performing arithmetic on timecodes + with frame-accurate precision. + + * :ref:`scenedetect.scene_detector 🌐 `: Contains :py:class:`SceneDetector ` base class for implementing scene detection algorithms. + + * :ref:`scenedetect.stats_manager 🧮 `: Contains :py:class:`StatsManager ` class for caching frame metrics and loading/saving them to disk in CSV format for analysis. Also used as a persistent cache to make multiple passes on the same video significantly faster. + + * :ref:`scenedetect.platform 🐱‍💻 `: Logging and utility functions. + + +Most types/functions are also available directly from the `scenedetect` package to make imports simpler. + +.. note:: + + The PySceneDetect API is still under development. It is recommended that you pin the `scenedetect` version in your requirements to below the next major release: + + .. code:: python + + scenedetect<0.7 + + +.. _scenedetect-quickstart: + +======================================================================= +Quickstart +======================================================================= + +To get started, the :py:func:`scenedetect.detect` function takes a path to a video and a :ref:`scene detector object`, and returns a list of start/end timecodes. For detecting fast cuts (shot changes), we use the :py:class:`ContentDetector `: + +.. code:: python + + from scenedetect import detect, ContentDetector + scene_list = detect('my_video.mp4', ContentDetector()) + +``scene_list`` is now a list of :py:class:`FrameTimecode ` pairs representing the start/end of each scene (try calling ``print(scene_list)``). Note that you can set ``show_progress=True`` when calling :py:func:`detect ` to display a progress bar with estimated time remaining. + +Next, let's print the scene list in a more readable format by iterating over it: + +.. code:: python + + for i, scene in enumerate(scene_list): + print('Scene %2d: Start %s / Frame %d, End %s / Frame %d' % ( + i+1, + scene[0].get_timecode(), scene[0].get_frames(), + scene[1].get_timecode(), scene[1].get_frames(),)) + +Now that we know where each scene is, we can also :ref:`split the input video ` automatically using `ffmpeg` (`mkvmerge` is also supported): + +.. code:: python + + from scenedetect import detect, ContentDetector, split_video_ffmpeg + scene_list = detect('my_video.mp4', ContentDetector()) + split_video_ffmpeg('my_video.mp4', scene_list) + +This is just a small snippet of what PySceneDetect offers. The library is very modular, and can integrate with most application workflows easily. + +In the next example, we show how the library components can be used to create a more customizable scene cut/shot detection pipeline. Additional demonstrations/recipes can be found in the `tests/test_api.py `_ file. + + +.. _scenedetect-detailed_example: + +======================================================================= +Example +======================================================================= + +In this example, we create a function ``find_scenes()`` which will load a video, detect the scenes, and return a list of tuples containing the (start, end) timecodes of each detected scene. Note that you can modify the `threshold` argument to modify the sensitivity of the :py:class:`ContentDetector `, or use other detection algorithms (e.g. :py:class:`ThresholdDetector `, :py:class:`AdaptiveDetector `). + +.. code:: python + + from scenedetect import SceneManager, open_video, ContentDetector + + def find_scenes(video_path, threshold=27.0): + video = open_video(video_path) + scene_manager = SceneManager() + scene_manager.add_detector( + ContentDetector(threshold=threshold)) + # Detect all scenes in video from current position to end. + scene_manager.detect_scenes(video) + # `get_scene_list` returns a list of start/end timecode pairs + # for each scene that was found. + return scene_manager.get_scene_list() + +Using a :py:class:`SceneManager ` directly allows tweaking the Parameters passed to :py:meth:`detect_scenes ` including setting a limit to the number of frames to process, which is useful for live streams/camera devices. You can also combine detection algorithms or create new ones from scratch. + +For a more advanced example of using the PySceneDetect API to with a stats file (to save per-frame metrics to disk and/or speed up multiple passes of the same video), take a look at the :ref:`example in the SceneManager reference`. + +In addition to module-level examples, demonstrations of some common use cases can be found in the `tests/test_api.py `_ file. + + +======================================================================= +Migrating From 0.5 +======================================================================= + +PySceneDetect 0.6 introduces several breaking changes which are incompatible with 0.5. See :ref:`Migration Guide ` for details on how to update your application. In addition, demonstrations of common use cases can be found in the `tests/test_api.py `_ file. + + +======================================================================= +Module-Level Functions +======================================================================= + + +`detect` +=============================================================== + +.. autofunction:: scenedetect.detect + +`open_video` +=============================================================== +.. autofunction:: scenedetect.open_video + + +======================================================================= +Logging +======================================================================= + +PySceneDetect outputs messages to a logger named ``pyscenedetect`` which does not have any default handlers. You can use :py:func:`scenedetect.init_logger ` with ``show_stdout=True`` or specify a log file (verbosity can also be specified) to attach some common handlers, or use ``logging.getLogger('pyscenedetect')`` and attach log handlers manually. diff --git a/docs/0.6.1/_sources/api/backends.rst.txt b/docs/0.6.1/_sources/api/backends.rst.txt new file mode 100644 index 00000000..a9405a6c --- /dev/null +++ b/docs/0.6.1/_sources/api/backends.rst.txt @@ -0,0 +1,37 @@ + +.. _scenedetect-backends: + +---------------------------------------- +Backends +---------------------------------------- + +.. automodule:: scenedetect.backends + :members: + :undoc-members: + + +========================================= +OpenCV +========================================= + +.. automodule:: scenedetect.backends.opencv + :members: + :undoc-members: + + +========================================= +PyAV +========================================= + +.. automodule:: scenedetect.backends.pyav + :members: + :undoc-members: + + +========================================= +MoviePy +========================================= + +.. automodule:: scenedetect.backends.moviepy + :members: + :undoc-members: diff --git a/docs/0.6.1/_sources/api/detectors.rst.txt b/docs/0.6.1/_sources/api/detectors.rst.txt new file mode 100644 index 00000000..2ff02890 --- /dev/null +++ b/docs/0.6.1/_sources/api/detectors.rst.txt @@ -0,0 +1,38 @@ + +.. _scenedetect-detectors: + +---------------------------------------- +Detection Algorithms +---------------------------------------- + +.. automodule:: scenedetect.detectors + :members: + :undoc-members: + + +========================================= +ContentDetector +========================================= + +.. automodule:: scenedetect.detectors.content_detector + :members: + :undoc-members: + + +========================================= +AdaptiveDetector +========================================= + +.. automodule:: scenedetect.detectors.adaptive_detector + :members: + :undoc-members: + + +========================================= +ThresholdDetector +========================================= + +.. automodule:: scenedetect.detectors.threshold_detector + :members: + :undoc-members: + diff --git a/docs/0.6.1/_sources/api/frame_timecode.rst.txt b/docs/0.6.1/_sources/api/frame_timecode.rst.txt new file mode 100644 index 00000000..bef60c13 --- /dev/null +++ b/docs/0.6.1/_sources/api/frame_timecode.rst.txt @@ -0,0 +1,22 @@ + +.. _scenedetect-frame_timecode: + +--------------------------------------------------------------- +FrameTimecode +--------------------------------------------------------------- + +.. automodule:: scenedetect.frame_timecode + +=============================================================== +``FrameTimecode`` Class +=============================================================== + +.. autoclass:: scenedetect.frame_timecode.FrameTimecode + :members: + :undoc-members: + +=============================================================== +Constants +=============================================================== + +.. autodata:: scenedetect.frame_timecode.MAX_FPS_DELTA diff --git a/docs/0.6.1/_sources/api/migration_guide.rst.txt b/docs/0.6.1/_sources/api/migration_guide.rst.txt new file mode 100644 index 00000000..9385cd43 --- /dev/null +++ b/docs/0.6.1/_sources/api/migration_guide.rst.txt @@ -0,0 +1,136 @@ + +.. _scenedetect-migration_guide: + +--------------------------------------------------------------- +Migration Guide +--------------------------------------------------------------- + +This page details how to transition a program written using PySceneDetect 0.5 to the new 0.6 API. It is recommended to review the new :ref:`Quickstart ` and :ref:`Example ` sections first, as they should cover the majority of use cases. Also see `tests/test_api.py `_ for a set of demonstrations covering many high level use cases. + +PySceneDetect v0.6 is a major step towards a more stable and simplified API. The biggest change to existing workflows is how video input is handled, and that Python 3.6 or above is now required. + +This page covers commonly used APIs which require updates to work with v0.6. Note that this page is not an exhaustive set of changes. For a complete list of breaking API changes, see `the changelog `_. + +In some places, a backwards compatibility layer has been added to avoid breaking most applications upon release. This should not be relied upon, and will be removed in the future. You can call ``scenedetect.platform.init_logger(show_stdout=True)`` or attach a custom log handler to the ``'pyscenedetect'`` logger to help find these cases. + + +=============================================================== +`VideoManager` Class +=============================================================== + +`VideoManager` has been deprecated and replaced with :py:mod:`scenedetect.backends`. For most applications, the :py:func:`open_video ` function should be used instead: + +.. code:: python + + from scenedetect import open_video + video = open_video(video.mp4') + +The resulting object can then be passed to a :py:class:`SceneManager ` when calling :py:meth:`detect_scenes `, or any other function/method that used to take a `VideoManager`, e.g.: + +.. code:: python + + from scenedetect import open_video, SceneManager, ContentDetector + video = open_video('video.mp4') + scene_manager = SceneManager() + scene_manager.add_detector(ContentDetector(threshold=threshold)) + scene_manager.detect_scenes(video) + print(scene_manager.get_scene_list()) + +See :py:mod:`scenedetect.backends` for examples of how to create specific backends. Where previously a list of paths was accepted, now only a single string should be provided. + + +Seeking and Start/End Times +=============================================================== + +Instead of setting the start time via the `VideoManager`, now :py:meth:`seek ` to the starting time on the :py:class:`VideoStream ` object. + +Instead of setting the duration or end time via the `VideoManager`, now set the `duration` or `end_time` parameters when calling :py:meth:`detect_scenes `. + +.. code:: python + + from scenedetect import open_video, SceneManager, ContentDetector + video = open_video('video.mp4') + # Can be seconds (float), frame # (int), or FrameTimecode + start_time, end_time = 2.5, 5.0 + scene_manager = SceneManager() + scene_manager.add_detector(ContentDetector(threshold=threshold)) + video.seek(start_time) + # Note there is also a `duration` parameter that can also be set. + # If neither `duration` nor `end_time` is provided, the video will + # be processed from its current position until the end. + scene_manager.detect_scenes(video, end_time=end_time) + print(scene_manager.get_scene_list()) + + +=============================================================== +`SceneManager` Class +=============================================================== + +The first argument of the :py:meth:`detect_scenes ` method has been renamed to `video` and should now be a :py:class:`VideoStream ` object (see above). + + +=============================================================== +`save_images` Function +=============================================================== + +The second argument of :py:func:`save_images ` in :py:mod:`scenedetect.scene_manager` has been renamed from `video_manager` to `video`. + +The `downscale_factor` parameter has been removed from :py:func:`save_images ` (use the `scale` parameter instead). To achieve the same result as the previous version, set `scale` to `1.0 / downscale_factor`. + + +=============================================================== +`split_video_*` Functions +=============================================================== + +The the :py:mod:`scenedetect.video_splitter` functions :py:func:`split_video_ffmpeg ` and :py:func:`split_video_mkvmerge ` now only accept a single path as the input (first) argument. + +The `suppress_output` and `hide_progress` arguments to the :py:func:`split_video_ffmpeg ` and :py:func:`split_video_mkvmerge ` have been removed, and two new options have been added: + + * `suppress_output` is now `show_output`, default is `False` + * `hide_progress` is now `show_progress`, default is `False` + +This makes the API consistent with that of :py:class:`SceneManager `. + + +=============================================================== +`StatsManager` Class +=============================================================== + +The :py:func:`save_to_csv ` and :py:func:`load_from_csv ` methods now accept either a `path` or an open `file` handle. + +The `base_timecode` argument has been removed from :py:func:`save_to_csv `. It is no longer required. + + +=============================================================== +`AdaptiveDetector` Class +=============================================================== + +The `video_manager` parameter has been removed and is no longer required when constructing an :py:class:`AdaptiveDetector ` object. + + +=============================================================== +Other +=============================================================== + +`ThresholdDetector` Class +=============================================================== + +The `block_size` argument has been removed from the :py:class:`ThresholdDetector ` constructor. It is no longer required. + + +`ContentDetector` Class +=============================================================== + +The `calculate_frame_score` method of :py:class:`ContentDetector ` has been renamed to :py:meth:`_calculate_frame_score `. Use new global function :py:func:`calculate_frame_score ` to achieve the same result. + + +`MINIMUM_FRAMES_PER_SECOND_*` Constants +=============================================================== + +In :py:mod:`scenedetect.frame_timecode` the constants `MINIMUM_FRAMES_PER_SECOND_FLOAT` and `MINIMUM_FRAMES_PER_SECOND_DELTA_FLOAT` have been replaced with :py:data:`MAX_FPS_DELTA `. + + +`get_aspect_ratio` Function +=============================================================== + + The `get_aspect_ratio` function has been removed from `scenedetect.platform`. Use the :py:attr:`aspect_ratio ` property from the :py:class:`VideoStream ` object instead. diff --git a/docs/0.6.1/_sources/api/platform.rst.txt b/docs/0.6.1/_sources/api/platform.rst.txt new file mode 100644 index 00000000..663a4efc --- /dev/null +++ b/docs/0.6.1/_sources/api/platform.rst.txt @@ -0,0 +1,29 @@ + +.. _scenedetect-platform: + +--------------------------------------------------------------- +Platform & Logging +--------------------------------------------------------------- + +.. automodule:: scenedetect.platform + + +=============================================================== +Functions +=============================================================== + +.. autofunction:: scenedetect.platform.get_and_create_path + +.. autofunction:: scenedetect.platform.get_file_name + +.. autofunction:: scenedetect.platform.init_logger + +.. autofunction:: scenedetect.platform.invoke_command + + +=============================================================== +Exceptions +=============================================================== + +.. autoexception:: scenedetect.platform.CommandTooLong + diff --git a/docs/0.6.1/_sources/api/scene_detector.rst.txt b/docs/0.6.1/_sources/api/scene_detector.rst.txt new file mode 100644 index 00000000..b9a2dc78 --- /dev/null +++ b/docs/0.6.1/_sources/api/scene_detector.rst.txt @@ -0,0 +1,11 @@ + +.. _scenedetect-scene_detector: + +------------------------------------------------- +SceneDetector +------------------------------------------------- + +.. automodule:: scenedetect.scene_detector + :members: + :undoc-members: + :private-members: diff --git a/docs/0.6.1/_sources/api/scene_manager.rst.txt b/docs/0.6.1/_sources/api/scene_manager.rst.txt new file mode 100644 index 00000000..cac35af4 --- /dev/null +++ b/docs/0.6.1/_sources/api/scene_manager.rst.txt @@ -0,0 +1,56 @@ + +.. _scenedetect-scene_manager: + +----------------------------------------------------------------------- +SceneManager +----------------------------------------------------------------------- + +.. automodule:: scenedetect.scene_manager + + +.. _scenemanager-example: + +======================================================================= +Storing Per-Frame Statistics +======================================================================= + +A `SceneManager` can use an optional :py:class:`StatsManager ` to save per-frame statistics to disk: + +.. code:: python + + from scenedetect import open_video, ContentDetector, SceneManager, StatsManager + video = open_video(test_video_file) + scene_manager = SceneManager(stats_manager=StatsManager()) + scene_manager.add_detector(ContentDetector()) + scene_manager.detect_scenes(video=video) + scene_list = scene_manager.get_scene_list() + print_scenes(scene_list=scene_list) + # Save per-frame statistics to disk. + scene_manager.stats_manager.save_to_csv(csv_file=STATS_FILE_PATH) + + +The statsfile can be used to find a better threshold for certain inputs, or perform further statistical analysis. The use of a :py:class:`StatsManager ` also allows certain detectors to operate faster on subsequent passes by caching calculations. Statsfiles can be persisted on disk and loaded again, which helps avoid unnecessary calculations in applications where multiple passes are expected (e.g. interactively selecting a threshold). + + +======================================================================= +``SceneManager`` Class +======================================================================= + +.. autoclass:: scenedetect.scene_manager.SceneManager + :members: + :undoc-members: + + +.. _scenedetect-scene_manager-functions: + +======================================================================= +``scene_manager`` Functions +======================================================================= + +.. autofunction:: scenedetect.scene_manager.save_images + +.. autofunction:: scenedetect.scene_manager.write_scene_list + +.. autofunction:: scenedetect.scene_manager.write_scene_list_html + +.. autofunction:: scenedetect.scene_manager.get_scenes_from_cuts diff --git a/docs/0.6.1/_sources/api/stats_manager.rst.txt b/docs/0.6.1/_sources/api/stats_manager.rst.txt new file mode 100644 index 00000000..6765dc47 --- /dev/null +++ b/docs/0.6.1/_sources/api/stats_manager.rst.txt @@ -0,0 +1,28 @@ + +.. _scenedetect-stats_manager: + +----------------------------------------------------------------------- +StatsManager +----------------------------------------------------------------------- + +.. automodule:: scenedetect.stats_manager + + +======================================================================= +``StatsManager`` Class +======================================================================= + +.. autoclass:: scenedetect.stats_manager.StatsManager + :members: + :undoc-members: + + +======================================================================= +Exceptions +======================================================================= + +.. autoexception:: scenedetect.stats_manager.FrameMetricRegistered + +.. autoexception:: scenedetect.stats_manager.FrameMetricNotRegistered + +.. autoexception:: scenedetect.stats_manager.StatsFileCorrupt diff --git a/docs/0.6.1/_sources/api/video_splitter.rst.txt b/docs/0.6.1/_sources/api/video_splitter.rst.txt new file mode 100644 index 00000000..79d404d7 --- /dev/null +++ b/docs/0.6.1/_sources/api/video_splitter.rst.txt @@ -0,0 +1,11 @@ + + +.. _scenedetect-video_splitter: + +--------------------------------------------------------------- +Video Splitting +--------------------------------------------------------------- + +.. automodule:: scenedetect.video_splitter + :members: + :undoc-members: diff --git a/docs/0.6.1/_sources/api/video_stream.rst.txt b/docs/0.6.1/_sources/api/video_stream.rst.txt new file mode 100644 index 00000000..c2806287 --- /dev/null +++ b/docs/0.6.1/_sources/api/video_stream.rst.txt @@ -0,0 +1,41 @@ + +.. _scenedetect-video_stream: + +--------------------------------------------------------------- +VideoStream +--------------------------------------------------------------- + +.. automodule:: scenedetect.video_stream + + +=============================================================== +``VideoStream`` Interface +=============================================================== + +.. autoclass:: scenedetect.video_stream.VideoStream + :members: + :undoc-members: + + +=============================================================== +``video_stream`` Functions and Constants +=============================================================== + +The following functions and constants are available in the ``scenedetect.video_stream`` module. + +.. autodata:: scenedetect.video_stream.DEFAULT_MIN_WIDTH + +.. autofunction:: scenedetect.video_stream.compute_downscale_factor + + +=============================================================== +Exceptions +=============================================================== + +.. autoexception:: scenedetect.video_stream.VideoOpenFailure + +.. autoexception:: scenedetect.video_stream.SeekError + + + + diff --git a/docs/0.6.1/_sources/cli/backends.rst.txt b/docs/0.6.1/_sources/cli/backends.rst.txt new file mode 100644 index 00000000..4a457451 --- /dev/null +++ b/docs/0.6.1/_sources/cli/backends.rst.txt @@ -0,0 +1,29 @@ + +.. _cli-backends: + +*********************************************************************** +Backends +*********************************************************************** + +PySceneDetect supports multiple backends for video input. Some can be configured by using :ref:`a config file `. Installed backends can be verified by running ``scenedetect version --all``. + + +======================================================================= +OpenCV +======================================================================= + +*[Default]* +The `OpenCV `_ backend (usually `opencv-python `_) uses an underlying ``cv2.VideoCapture object`` for video input. Can be used by specifying ``-b opencv`` via command line, or setting ``backend = opencv`` under the ``[global]`` section of your :ref:`config file `. + +It is mostly reliable and fast, although can occasionally run into issues processing videos with multiple audio tracks or small amounts of frame corruption. You can use a custom version of the ``cv2`` package, or install either the `opencv-python` or `opencv-python-headless` packages from `pip`. + +The OpenCV backend also supports image sequences as inputs (e.g. ``frame%02d.jpg`` if you want to load frame001.jpg, frame002.jpg, frame003.jpg...). Make sure to specify the framerate manually (``-f``/``--framerate``) to ensure accurate timing calculations. + + +======================================================================= +PyAV +======================================================================= + +The `PyAV `_ backend (package `av _`) is a more robust backend that handles multiple audio tracks and frame decode errors gracefully. + +This backend can be used by specifying ``-b pyav`` via command line, or setting ``backend = pyav`` under the ``[global]`` section of your :ref:`config file `. diff --git a/docs/0.6.1/_sources/cli/commands.rst.txt b/docs/0.6.1/_sources/cli/commands.rst.txt new file mode 100644 index 00000000..09648052 --- /dev/null +++ b/docs/0.6.1/_sources/cli/commands.rst.txt @@ -0,0 +1,356 @@ + +.. _cli-commands: + +*********************************************************************** +Commands +*********************************************************************** + +The following commands are available when using ``scenedetect``. Several commands can be combined together (the order does not matter) to control various input/output options. + +The following is a list of the available commands along with a brief description of the command's function and an example. + + +Help/information commands: + + - :ref:`help ` - Prints help and usage information for commands + ``help`` or ``help split-video`` or ``help all`` + - :ref:`about ` - Prints license and copyright information about PySceneDetect + - :ref:`version ` - Print PySceneDetect version Number + +Input/output commands (applies to input videos and detected scenes): + + - :ref:`time