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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/pinned-assets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: pinned-assets

on:
schedule:
- cron: "17 6 * * 1" # Mondays at 06:17 UTC
workflow_dispatch:

permissions:
contents: read

jobs:
availability:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: "3.11"
- name: Check both pinned Linux FFmpeg assets are available
env:
HFLOW_NETWORK_TESTS: "1"
run: uv run --locked pytest tests/test_ffmpeg.py::test_pinned_release_assets_available -q
4 changes: 4 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ jobs:
echo "Release tag ${GITHUB_REF_NAME} must equal v${package_version}." >&2
exit 1
fi
- name: Check both pinned Linux FFmpeg assets are available
env:
HFLOW_NETWORK_TESTS: "1"
run: uv run --locked pytest tests/test_ffmpeg.py::test_pinned_release_assets_available -q
- name: Build distributions
run: uv build
- name: Smoke-test the wheel in a clean environment
Expand Down
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,24 @@ HFLOW_TEST_BUCKET_URL=gs://your-bucket/tmp-prefix uv run pytest tests/test_stora
HFLOW_MEDIAPIPE_TESTS=1 uv run --extra mediapipe pytest tests/test_mediapipe_hands.py -q
```

Normal CI uses system FFmpeg and does not check the managed Linux download.
For a cheap availability check of both pinned Linux archives, run:

```bash
HFLOW_NETWORK_TESTS=1 uv run pytest tests/test_ffmpeg.py::test_pinned_release_assets_available -q
```

This follows release-asset redirects with HTTP HEAD, without downloading the
archives. The `pinned-assets` workflow runs it weekly on Mondays and supports
manual dispatch. The publish workflow repeats it before building distributions.
The separate
`test_real_pinned_download_and_version` downloads into an isolated cache and
executes both binaries for the host architecture. Run it when changing the
pin, and independently download, hash, and inspect the other architecture's
archive. Select the last successful BtbN build of a completed month from its
release list: these are retained for two years, while ordinary daily builds
expire quickly. A tag's calendar date alone cannot establish retention.

The MediaPipe one brings its own OpenCV, and the OpenCV wheels share one
`cv2/` directory, so syncing back out can leave `import cv2` broken while
`uv sync` still calls the environment correct. One command puts it back:
Expand Down
6 changes: 5 additions & 1 deletion src/hflow/behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,8 @@
# source_uri as the data-root-prefixed and absolute spellings. The corrected
# provenance changes canonical bytes for recordings previously processed under
# the cwd-relative absolute identity, so they must not share a pipeline identity.
TRANSFORM_BEHAVIOR_VERSION: str = "7"
#
# "8": the managed Linux FFmpeg pin now uses BtbN's retained August 2026
# month-final build. Its version stamp changes canonical provenance whenever
# video is transcoded, so those outputs must not share the old pipeline identity.
TRANSFORM_BEHAVIOR_VERSION: str = "8"
18 changes: 10 additions & 8 deletions src/hflow/ffmpeg/_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
1. Explicit user override via ``HFLOW_FFMPEG`` (a deliberate choice;
used by tests and users with their own managed builds).
2. The pinned static build, auto-downloaded once into the user cache dir and
sha256-verified (Linux x86_64/aarch64 from BtbN/FFmpeg-Builds immutable
sha256-verified (Linux x86_64/aarch64 from BtbN/FFmpeg-Builds dated
release assets; Windows runs the Linux path under WSL2). On Linux there is
deliberately NO PATH fallback: either the pinned build resolves or we raise
with instructions, so two Linux machines can never silently disagree.
Expand Down Expand Up @@ -55,11 +55,13 @@
# BOTH in the versioned install dir is the completion condition.
_PINNED_BINARY_NAMES = ("ffmpeg", "ffprobe")

# The pinned build is a dated BtbN/FFmpeg-Builds autobuild tag (NOT the
# floating "latest" tag): dated release assets are immutable, so the URL and
# bytes below can never change underneath the sha256 pins.
PINNED_RELEASE_TAG = "autobuild-2026-08-16-13-00"
PINNED_VERSION_LABEL = "n8.1.2-44-g7c533d0f86"
# SHA256 pins prevent silently accepting changed bytes; they do not keep assets
# available. Choose the last successful build of a completed month from BtbN's
# release list (not just a calendar month-end date): it is retained for two years,
# while ordinary daily builds expire quickly. Refresh before that retention ends;
# never pin the floating "latest" tag.
PINNED_RELEASE_TAG = "autobuild-2026-08-31-13-27"
PINNED_VERSION_LABEL = "n8.1.2-50-g1a748fe2cd"

_RELEASE_DOWNLOAD_BASE_URL = (
f"https://github.com/BtbN/FFmpeg-Builds/releases/download/{PINNED_RELEASE_TAG}"
Expand All @@ -80,12 +82,12 @@ class PinnedBuild:
PINNED_BUILDS_BY_MACHINE: dict[str, PinnedBuild] = {
"x86_64": PinnedBuild(
url=f"{_RELEASE_DOWNLOAD_BASE_URL}/ffmpeg-{PINNED_VERSION_LABEL}-linux64-gpl-8.1.tar.xz",
sha256_hex="17780994c4679806fb227676f66a0af30c6379afc770324829f48f2a379be558",
sha256_hex="c733b4b2951e5957e15505f788b2c65a7a41b6da4b289e295852cc38079b4d2b",
archive_bin_dir=f"ffmpeg-{PINNED_VERSION_LABEL}-linux64-gpl-8.1/bin",
),
"aarch64": PinnedBuild(
url=f"{_RELEASE_DOWNLOAD_BASE_URL}/ffmpeg-{PINNED_VERSION_LABEL}-linuxarm64-gpl-8.1.tar.xz",
sha256_hex="e970a7dd450b440a21126a8bac3a1c95178b6ba05bee2465a4d2a586345c81ac",
sha256_hex="ae5da4f51b9052390f414005f8ab26c1eed1268f327cce7cb79aa076b29bd66e",
archive_bin_dir=f"ffmpeg-{PINNED_VERSION_LABEL}-linuxarm64-gpl-8.1/bin",
),
}
Expand Down
26 changes: 24 additions & 2 deletions tests/test_ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

The suite conftest pins ``HFLOW_FFMPEG`` to the system binary, so the
resolution tests below must clear the ``lru_cache`` around any environment
mutation. The one real-download test is opt-in via ``HFLOW_NETWORK_TESTS=1``.
mutation. Remote availability and real-download tests are opt-in via
``HFLOW_NETWORK_TESTS=1``.
"""

import hashlib
Expand All @@ -15,6 +16,7 @@
from collections.abc import Iterator
from pathlib import Path

import httpx2
import numpy as np
import pytest

Expand Down Expand Up @@ -298,6 +300,24 @@ def test_cache_dir_with_only_ffmpeg_is_healed_by_reinstall(
assert ffmpeg_path() == install_dir / "ffmpeg"


@pytest.mark.skipif(
os.environ.get("HFLOW_NETWORK_TESTS") != "1",
reason="network integration test; set HFLOW_NETWORK_TESTS=1 to run",
)
@pytest.mark.parametrize("machine", ["x86_64", "aarch64"])
def test_pinned_release_assets_available(machine: str) -> None:
"""Probe both remote pins without downloading either archive or using the cache."""
build = _binary.PINNED_BUILDS_BY_MACHINE[machine]
try:
# Preserve HEAD across GitHub's redirect, including on Python 3.11.
response = httpx2.head(build.url, follow_redirects=True, timeout=30)
except httpx2.HTTPError as error:
pytest.fail(f"pinned FFmpeg asset unavailable for {machine}: {build.url}: {error}")
assert response.status_code == 200, (
f"pinned FFmpeg asset unavailable for {machine}: {build.url} (HTTP {response.status_code})"
)


@pytest.mark.skipif(
os.environ.get("HFLOW_NETWORK_TESTS") != "1",
reason="network integration test; set HFLOW_NETWORK_TESTS=1 to run",
Expand All @@ -307,11 +327,13 @@ def test_cache_dir_with_only_ffmpeg_is_healed_by_reinstall(
reason="pinned builds exist for Linux x86_64/aarch64 only",
)
def test_real_pinned_download_and_version(
monkeypatch: pytest.MonkeyPatch, cleared_binary_caches: None
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, cleared_binary_caches: None
) -> None:
monkeypatch.delenv(FFMPEG_ENV_VAR, raising=False)
monkeypatch.delenv(FFPROBE_ENV_VAR, raising=False)
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "cache"))
resolved = ffmpeg_path()
assert resolved == _pinned_install_dir(platform.machine()) / "ffmpeg"
assert resolved.is_file()
version_line = ffmpeg_version()
assert PINNED_VERSION_LABEL in version_line
Expand Down