From 32b2cc1bc574ab1d7e5e834385653c720e2607d8 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Fri, 20 Feb 2026 18:23:45 -0500 Subject: [PATCH 1/9] QEMU emulation now replaced with native arm64 VM for container builds --- .github/workflows/container.yml | 135 ++++++++++++++++++++++++-------- container/opencv4.containerfile | 3 +- 2 files changed, 105 insertions(+), 33 deletions(-) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 5e91033..2296743 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -11,6 +11,10 @@ on: required: true type: string description: 'name of the container build file to use' + ref: + required: false + type: string + description: 'git ref to checkout (defaults to version)' workflow_dispatch: inputs: version: @@ -21,30 +25,45 @@ on: required: true type: string description: 'name of the container build file to use' + ref: + required: false + type: string + description: 'git ref to checkout (defaults to version)' env: REGISTRY: ghcr.io jobs: - build-and-push-image: - name: "🐳 Build and push image" - runs-on: ubuntu-latest + build: + name: "🐳 Build (${{ matrix.platform }})" + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm permissions: contents: read packages: write steps: + - name: "🔧 Prepare" + id: prepare + run: | + platform=${{ matrix.platform }} + echo "pair=${platform//\//-}" >> $GITHUB_OUTPUT + slug=$(echo "${{ inputs.buildfilename }}" | sed 's|[^a-zA-Z0-9]|-|g; s/^-*//; s/-*$//') + echo "slug=${slug}" >> $GITHUB_OUTPUT + - name: "🛍️ Checkout repository" uses: actions/checkout@v4 with: - ref: ${{ inputs.version }} - fetch-depth: 0 - - - name: "🎛 Set up QEMU" - uses: docker/setup-qemu-action@v3 + ref: ${{ inputs.ref || inputs.version }} - name: "👷 Set up Docker Buildx" - id: buildx uses: docker/setup-buildx-action@v3 - name: "🏷 Prepare OCI annotations" @@ -68,23 +87,11 @@ jobs: export nameonly="${filename%.*}" if [ ${nameonly} == ${filename} ]; then echo "SUFFIX=" >> $GITHUB_ENV ; else echo "SUFFIX=-${nameonly}" >> $GITHUB_ENV; fi - - name: "🏷 Get SDK version from latest tag" - id: sdkversion - run: | - export version=$(git describe --tags --abbrev=0) - echo "CLAMS_VERSION=${version}" >> $GITHUB_OUTPUT - - - name: "🏷 Prepare docker tags, labels" + - name: "🏷 Prepare docker labels" id: meta uses: docker/metadata-action@v5 - env: - CLAMS_VERSION: ${{ steps.sdkversion.outputs.CLAMS_VERSION }} with: images: ${{ env.REGISTRY }}/${{ github.repository }}${{ env.SUFFIX }} - tags: | - type=pep440,pattern={{version}},value=${{ env.CLAMS_VERSION }} - type=ref,event=tag - type=ref,event=pr labels: | ${{ env.EXISTING_LABELS }} @@ -95,18 +102,84 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: "🏗 Build and push image" - uses: docker/build-push-action@v5 - env: - CLAMS_VERSION: ${{ steps.sdkversion.outputs.CLAMS_VERSION }} + - name: "🏗 Build and push by digest" + id: build + uses: docker/build-push-action@v6 with: context: ${{ env.CONTEXT }} - platforms: linux/amd64,linux/arm64 + platforms: ${{ matrix.platform }} file: ${{ inputs.buildfilename }} - tags: ${{ steps.meta.outputs.tags }} - # using {{ steps.meta.outputs.labels }} doesn't work with multi-line variable ($EXISTING_LABLES) labels: ${{ env.DOCKER_METADATA_OUTPUT_LABELS }} build-args: | - clams_version=${{ env.CLAMS_VERSION }} - push: true + clams_version=${{ inputs.version }} + outputs: type=image,"name=${{ env.REGISTRY }}/${{ github.repository }}${{ env.SUFFIX }}",push-by-digest=true,name-canonical=true,push=true + + - name: "📤 Export digest" + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + - name: "📦 Upload digest" + uses: actions/upload-artifact@v4 + with: + name: digests-${{ steps.prepare.outputs.slug }}-${{ steps.prepare.outputs.pair }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + name: "🔗 Create multi-platform manifest" + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + packages: write + + steps: + - name: "🔧 Prepare" + id: prepare + run: | + slug=$(echo "${{ inputs.buildfilename }}" | sed 's|[^a-zA-Z0-9]|-|g; s/^-*//; s/-*$//') + echo "slug=${slug}" >> $GITHUB_OUTPUT + + - name: "🏷 Get image name suffix" + id: getsuffix + run: | + export filename=$(basename ${{ inputs.buildfilename }}) + export nameonly="${filename%.*}" + if [ ${nameonly} == ${filename} ]; then echo "SUFFIX=" >> $GITHUB_ENV ; else echo "SUFFIX=-${nameonly}" >> $GITHUB_ENV; fi + + - name: "📥 Download digests" + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-${{ steps.prepare.outputs.slug }}-* + merge-multiple: true + + - name: "👷 Set up Docker Buildx" + uses: docker/setup-buildx-action@v3 + + - name: "🏷 Prepare docker tags" + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ github.repository }}${{ env.SUFFIX }} + tags: | + type=pep440,pattern={{version}},value=${{ inputs.version }} + type=ref,event=tag + type=ref,event=pr + + - name: "🔏 Log in to registry" + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: "🔗 Create manifest list and push" + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY }}/${{ github.repository }}${{ env.SUFFIX }}@sha256:%s ' *) diff --git a/container/opencv4.containerfile b/container/opencv4.containerfile index 4895a7d..717efe1 100644 --- a/container/opencv4.containerfile +++ b/container/opencv4.containerfile @@ -28,8 +28,7 @@ RUN cmake \ -D BUILD_PERF_TESTS=OFF \ -D BUILD_TESTS=OFF \ -D CMAKE_INSTALL_PREFIX=/usr/local \ - -D OPENCV_EXTRA_MODULES_PATH=${OPENCV_EXTRA_MODULES_PATH} \ - -D BUILD_opencv_python3=ON \ + -D OPENCV_EXTRA_MODULES_PATH=${OPENCV_EXTRA_PATH}/modules \ -D BUILD_opencv_python3=OFF \ -D PYTHON3_EXECUTABLE=$(which python3) \ -D PYTHON_DEFAULT_EXECUTABLE=$(which python3) \ From f5960dcd9d6f68729b2a3c82df0e64d0d4516762 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Fri, 20 Feb 2026 18:24:29 -0500 Subject: [PATCH 2/9] drop tf2-hf images and update transformers to v5 --- .github/workflows/containers.yml | 27 -------------------------- container/ffmpeg-hf.containerfile | 2 +- container/ffmpeg-tf2-hf.containerfile | 7 ------- container/hf.containerfile | 2 +- container/opencv4-hf.containerfile | 2 +- container/opencv4-tf2-hf.containerfile | 7 ------- container/tf2-hf.containerfile | 7 ------- 7 files changed, 3 insertions(+), 51 deletions(-) delete mode 100644 container/ffmpeg-tf2-hf.containerfile delete mode 100644 container/opencv4-tf2-hf.containerfile delete mode 100644 container/tf2-hf.containerfile diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index bc45b7a..2c9160c 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -134,15 +134,6 @@ jobs: buildfilename: './container/opencv4-torch2.containerfile' version: ${{ needs.set-version.outputs.version }} - call-build-tf2-hf: - name: "🤙 Call container workflow with `tf2-hf`" - needs: ['set-version', 'call-build-tf2'] - uses: ./.github/workflows/container.yml - secrets: inherit - with: - buildfilename: './container/tf2-hf.containerfile' - version: ${{ needs.set-version.outputs.version }} - call-build-hf: name: "🤙 Call container workflow with `hf`" needs: ['set-version', 'call-build-torch2'] @@ -152,15 +143,6 @@ jobs: buildfilename: './container/hf.containerfile' version: ${{ needs.set-version.outputs.version }} - call-build-ffmpeg-tf2-hf: - name: "🤙 Call container workflow with `ffmpeg-tf2-hf`" - needs: ['set-version', 'call-build-ffmpeg-tf2'] - uses: ./.github/workflows/container.yml - secrets: inherit - with: - buildfilename: './container/ffmpeg-tf2-hf.containerfile' - version: ${{ needs.set-version.outputs.version }} - call-build-ffmpeg-hf: name: "🤙 Call container workflow with `ffmpeg-hf`" needs: ['set-version', 'call-build-ffmpeg-torch2'] @@ -170,15 +152,6 @@ jobs: buildfilename: './container/ffmpeg-hf.containerfile' version: ${{ needs.set-version.outputs.version }} - call-build-opencv4-tf2-hf: - name: "🤙 Call container workflow with `opencv4-tf2-hf`" - needs: ['set-version', 'call-build-opencv4-tf2'] - uses: ./.github/workflows/container.yml - secrets: inherit - with: - buildfilename: './container/opencv4-tf2-hf.containerfile' - version: ${{ needs.set-version.outputs.version }} - call-build-opencv4-hf: name: "🤙 Call container workflow with `opencv4-hf`" needs: ['set-version', 'call-build-opencv4-torch2'] diff --git a/container/ffmpeg-hf.containerfile b/container/ffmpeg-hf.containerfile index 51a2ab3..0a0dc6b 100644 --- a/container/ffmpeg-hf.containerfile +++ b/container/ffmpeg-hf.containerfile @@ -2,6 +2,6 @@ ARG clams_version FROM ghcr.io/clamsproject/clams-python-ffmpeg-torch2:$clams_version LABEL org.opencontainers.image.description="clams-python-ffmpeg-hf image is shipped with clams-python, ffmpeg, and vairous huggingface libraries (PyTorch backend)" -RUN pip install --no-cache-dir transformers[torch,tokenizers]==4.* +RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* RUN pip install --no-cache-dir datasets diff --git a/container/ffmpeg-tf2-hf.containerfile b/container/ffmpeg-tf2-hf.containerfile deleted file mode 100644 index 7d7a9dd..0000000 --- a/container/ffmpeg-tf2-hf.containerfile +++ /dev/null @@ -1,7 +0,0 @@ -ARG clams_version -FROM ghcr.io/clamsproject/clams-python-ffmpeg-tf2:$clams_version -LABEL org.opencontainers.image.description="clams-python-ffmpeg-tf2-hf image is shipped with clams-python, ffmpeg, tensorflow2, and vairous huggingface libraries" - -RUN pip install --no-cache-dir transformers[tf,tokenizers]==4.* -RUN pip install --no-cache-dir datasets - diff --git a/container/hf.containerfile b/container/hf.containerfile index 4de3e48..77c9d8e 100644 --- a/container/hf.containerfile +++ b/container/hf.containerfile @@ -2,6 +2,6 @@ ARG clams_version FROM ghcr.io/clamsproject/clams-python-torch2:$clams_version LABEL org.opencontainers.image.description="clams-python-hf image is shipped with clams-python and vairous huggingface libraries (PyTorch backend)" -RUN pip install --no-cache-dir transformers[torch,tokenizers]==4.* +RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* RUN pip install --no-cache-dir datasets diff --git a/container/opencv4-hf.containerfile b/container/opencv4-hf.containerfile index b28670f..a1b4841 100644 --- a/container/opencv4-hf.containerfile +++ b/container/opencv4-hf.containerfile @@ -2,6 +2,6 @@ ARG clams_version FROM ghcr.io/clamsproject/clams-python-opencv4-torch2:$clams_version LABEL org.opencontainers.image.description="clams-python-opencv4-hf image is shipped with clams-python, opencv4 (ffmpeg backend), and vairous huggingface libraries (PyTorch backend)" -RUN pip install --no-cache-dir transformers[torch,tokenizers]==4.* +RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* RUN pip install --no-cache-dir datasets diff --git a/container/opencv4-tf2-hf.containerfile b/container/opencv4-tf2-hf.containerfile deleted file mode 100644 index ea0a3dd..0000000 --- a/container/opencv4-tf2-hf.containerfile +++ /dev/null @@ -1,7 +0,0 @@ -ARG clams_version -FROM ghcr.io/clamsproject/clams-python-opencv4-tf2:$clams_version -LABEL org.opencontainers.image.description="clams-python-opencv4-tf2-hf image is shipped with clams-python, opencv4 (ffmpeg backend), tensorflow2, and vairous huggingface libraries" - -RUN pip install --no-cache-dir transformers[tf,tokenizers]==4.* -RUN pip install --no-cache-dir datasets - diff --git a/container/tf2-hf.containerfile b/container/tf2-hf.containerfile deleted file mode 100644 index 6b0fe7d..0000000 --- a/container/tf2-hf.containerfile +++ /dev/null @@ -1,7 +0,0 @@ -ARG clams_version -FROM ghcr.io/clamsproject/clams-python-tf2:$clams_version -LABEL org.opencontainers.image.description="clams-python-tf2-hf image is shipped with clams-python, tensorflow2, and vairous huggingface libraries" - -RUN pip install --no-cache-dir transformers[tf,tokenizers]==4.* -RUN pip install --no-cache-dir datasets - From 431d23e23da40b2e32785c95da2a0419450f58bb Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Tue, 10 Mar 2026 16:54:40 -0400 Subject: [PATCH 3/9] integration with new basic TimeFrame sampling built-ins in mmif-python --- clams/app/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/clams/app/__init__.py b/clams/app/__init__.py index 9f8b27b..9ec134b 100644 --- a/clams/app/__init__.py +++ b/clams/app/__init__.py @@ -13,6 +13,10 @@ from typing import Union, Any, Optional, Dict, List, Tuple from mmif import Mmif, Document, DocumentTypes, View +from mmif.utils.video_document_helper import ( + SamplingMode, SAMPLING_MODE_DESCRIPTIONS, SAMPLING_MODE_DEFAULT, + _sampling_mode, +) from mmif.utils.cli.describe import generate_param_hash # pytype: disable=import-error from clams.appmetadata import AppMetadata, real_valued_primitives, python_type, map_param_kv_delimiter @@ -55,6 +59,18 @@ class ClamsApp(ABC): 'name': 'hwFetch', 'type': 'boolean', 'choices': None, 'default': False, 'multivalued': False, 'description': 'The hardware information (architecture, GPU and vRAM) will be recorded in the view metadata', }, + { + 'name': 'tfSamplingMode', 'type': 'string', + 'choices': [m.value for m in SamplingMode], + 'default': SAMPLING_MODE_DEFAULT.value, + 'multivalued': False, + 'description': 'Sampling mode for TimeFrame annotations. ' + 'Has no effect when the app does not process TimeFrames. ' + + ' '.join( + f'"{m.value}" {SAMPLING_MODE_DESCRIPTIONS[m]}' + for m in SamplingMode + ), + }, ] # this key is used to store users' raw input params in the parameter dict @@ -148,6 +164,9 @@ def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) -> refined = self._refine_params(**runtime_params) self.logger.debug(f"Refined parameters: {refined}") pretty = refined.get('pretty', False) + sampling_mode_str = refined.pop('tfSamplingMode', None) + if sampling_mode_str is not None: + _sampling_mode.set(SamplingMode(sampling_mode_str)) t = datetime.now() with warnings.catch_warnings(record=True) as ws: annotated, cuda_profiler = self._profile_cuda_memory(self._annotate)(mmif, **refined) From ce9868866dc986790f8c13c1ffe9b26f0e6f0a65 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Tue, 10 Mar 2026 17:28:20 -0400 Subject: [PATCH 4/9] added for-devs comment on `tfSamplingMode` --- build-tools/docs.py | 13 +++++++++---- clams/app/__init__.py | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/build-tools/docs.py b/build-tools/docs.py index 682c478..3ef8cce 100644 --- a/build-tools/docs.py +++ b/build-tools/docs.py @@ -14,11 +14,13 @@ def run_command(command, cwd=None, check=True, env=None): sys.exit(result.returncode) return result -def build_docs_local(source_dir: Path): +def build_docs_local(source_dir: Path, output_dir: Path = None): """ Builds documentation for the provided source directory. Assumes it's running in an environment with necessary tools. """ + if output_dir is None: + output_dir = source_dir / "docs-test" print("--- Running in Local Build Mode ---") # 1. Generate source code and install in editable mode. @@ -49,7 +51,7 @@ def build_docs_local(source_dir: Path): # 3. Build the documentation using Sphinx. print("\n--- Step 3: Building Sphinx documentation ---") docs_source_dir = source_dir / "documentation" - docs_build_dir = source_dir / "docs-test" + docs_build_dir = output_dir # Schema generation is now handled in conf.py # schema_src = source_dir / "clams" / "appmetadata.jsonschema" @@ -74,9 +76,12 @@ def main(): parser = argparse.ArgumentParser( description="Build documentation for the clams-python project." ) + parser.add_argument( + '--output-dir', type=Path, default=None, + help='Output directory for built docs (default: docs-test)') args = parser.parse_args() - - build_docs_local(Path.cwd()) + + build_docs_local(Path.cwd(), output_dir=args.output_dir) if __name__ == "__main__": main() diff --git a/clams/app/__init__.py b/clams/app/__init__.py index 9ec134b..3f0f027 100644 --- a/clams/app/__init__.py +++ b/clams/app/__init__.py @@ -59,6 +59,12 @@ class ClamsApp(ABC): 'name': 'hwFetch', 'type': 'boolean', 'choices': None, 'default': False, 'multivalued': False, 'description': 'The hardware information (architecture, GPU and vRAM) will be recorded in the view metadata', }, + # tfSamplingMode is universal (not per-app) because it controls + # how vdh.extract_frames_by_mode() selects frames from TimeFrames. + # The value is intercepted in annotate() and pushed into a + # contextvars.ContextVar so that any vdh call inside _annotate() + # picks it up automatically — app developers never need to handle + # this parameter themselves. { 'name': 'tfSamplingMode', 'type': 'string', 'choices': [m.value for m in SamplingMode], From 8eafd718fc8532569c4812b1394851cf8cabf6d1 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Tue, 10 Mar 2026 22:07:54 -0400 Subject: [PATCH 5/9] updated mmif-python to 1.3.0, minor fixes and updated publish GHA --- .github/workflows/publish.yml | 53 +++++++++++++++++++++++++++++++---- clams/app/__init__.py | 2 +- requirements.txt | 2 +- tests/test_clamsapp.py | 4 +-- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cfa7082..07868a8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,13 +1,54 @@ -name: "📦 Publish (docs, PyPI)" +name: "📦 Publish (PyPI + docs)" -on: - push: - tags: +on: + push: + tags: - '[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 3.2.0)' + required: true jobs: - packge-and-upload: - name: "🤙 Call SDK publish workflow" + check-pypi: + name: "🔍 Check PyPI for existing package" + runs-on: ubuntu-latest + outputs: + exists: ${{ steps.check.outputs.exists }} + version: ${{ steps.check.outputs.version }} + steps: + - id: check + run: | + PACKAGE=$(echo "${{ github.repository }}" | cut -d/ -f2 | tr '-' '_') + VERSION=${{ inputs.version || github.ref_name }} + echo "version=$VERSION" >> $GITHUB_OUTPUT + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/$PACKAGE/$VERSION/json") + if [ "$STATUS" = "200" ]; then + echo "Package $PACKAGE $VERSION already exists on PyPI, skipping upload" + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "Package $PACKAGE $VERSION not found on PyPI, proceeding with upload" + echo "exists=false" >> $GITHUB_OUTPUT + fi + + publish-pypi: + name: "📦 Build and upload to PyPI" + needs: check-pypi + if: needs.check-pypi.outputs.exists == 'false' uses: clamsproject/.github/.github/workflows/sdk-publish.yml@main secrets: inherit + publish-docs: + name: "📖 Build and publish docs" + needs: [check-pypi, publish-pypi] + if: always() && needs.check-pypi.result == 'success' && needs.publish-pypi.result != 'failure' + uses: clamsproject/clamsproject.github.io/.github/workflows/sdk-docs.yml@main + with: + source_repo: clamsproject/clams-python + source_ref: ${{ needs.check-pypi.outputs.version }} + project_name: clams-python + build_command: 'python3 build-tools/docs.py --output-dir docs' + docs_output_dir: 'docs' + python_version: '3.11' + secrets: inherit diff --git a/clams/app/__init__.py b/clams/app/__init__.py index 3f0f027..1ca07fc 100644 --- a/clams/app/__init__.py +++ b/clams/app/__init__.py @@ -17,7 +17,7 @@ SamplingMode, SAMPLING_MODE_DESCRIPTIONS, SAMPLING_MODE_DEFAULT, _sampling_mode, ) -from mmif.utils.cli.describe import generate_param_hash # pytype: disable=import-error +from mmif.utils.workflow_helper import generate_param_hash # pytype: disable=import-error from clams.appmetadata import AppMetadata, real_valued_primitives, python_type, map_param_kv_delimiter logging.basicConfig( diff --git a/requirements.txt b/requirements.txt index 6394805..0f33917 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mmif-python==1.2.1 +mmif-python==1.3.0 Flask>=2 Flask-RESTful>=0.3.9 diff --git a/tests/test_clamsapp.py b/tests/test_clamsapp.py index 00faff8..b108461 100644 --- a/tests/test_clamsapp.py +++ b/tests/test_clamsapp.py @@ -253,14 +253,14 @@ def test_sign_view(self): args3 = {'undefined_param1': ['value1']} self.app.sign_view(v3, self.app._refine_params(**args3)) self.assertEqual(len(v3.metadata.parameters), 1) - self.assertEqual(len(v3.metadata.appConfiguration), 5) # universal (pretty, runningtime, hwfetch) + `raise_error` in metadata.py + `multivalued_param` + self.assertEqual(len(v3.metadata.appConfiguration), 6) # universal (pretty, runningTime, hwFetch, tfSamplingMode) + `raise_error` in metadata.py + `multivalued_param` self.assertEqual(v3.metadata.appConfiguration['multivalued_param'], []) v4 = m.new_view() multiple_values = ['value1', 'value2'] args4 = {'multivalued_param': multiple_values} self.app.sign_view(v4, self.app._refine_params(**args4)) self.assertEqual(len(v4.metadata.parameters), 1) - self.assertEqual(len(v4.metadata.appConfiguration), 5) + self.assertEqual(len(v4.metadata.appConfiguration), 6) self.assertEqual(len(v4.metadata.parameters['multivalued_param']), len(str(multiple_values))) def test_annotate(self): From 7c172df734e0369e5baf2b6c37b0efa3a3e90240 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Wed, 11 Mar 2026 14:26:08 -0400 Subject: [PATCH 6/9] added workaround for pytype error --- clams/app/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/clams/app/__init__.py b/clams/app/__init__.py index 1ca07fc..71d70f2 100644 --- a/clams/app/__init__.py +++ b/clams/app/__init__.py @@ -26,6 +26,16 @@ datefmt="%Y-%m-%d %H:%M:%S") +_sampling_mode_choices = [m.value for m in SamplingMode] +_sampling_mode_description = ( + 'Sampling mode for TimeFrame annotations. ' + 'Has no effect when the app does not process TimeFrames. ' + + ' '.join( + f'"{m.value}" {SAMPLING_MODE_DESCRIPTIONS[m]}' + for m in SamplingMode + ) +) + falsy_values = [ 'False', 'false', @@ -67,15 +77,10 @@ class ClamsApp(ABC): # this parameter themselves. { 'name': 'tfSamplingMode', 'type': 'string', - 'choices': [m.value for m in SamplingMode], + 'choices': _sampling_mode_choices, 'default': SAMPLING_MODE_DEFAULT.value, 'multivalued': False, - 'description': 'Sampling mode for TimeFrame annotations. ' - 'Has no effect when the app does not process TimeFrames. ' - + ' '.join( - f'"{m.value}" {SAMPLING_MODE_DESCRIPTIONS[m]}' - for m in SamplingMode - ), + 'description': _sampling_mode_description, }, ] From e593b5a110b9f5ec5cec83f90e65334f6fedb77a Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Wed, 11 Mar 2026 18:56:14 -0400 Subject: [PATCH 7/9] updated to the latest mmif-python sdk --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0f33917..6cd63d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mmif-python==1.3.0 +mmif-python==1.3.1 Flask>=2 Flask-RESTful>=0.3.9 From b80cbcdad75c17005c98126bc918ce701e06e69a Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Wed, 11 Mar 2026 20:20:34 -0400 Subject: [PATCH 8/9] Add tfSamplingMode documentation to developer tutorial (#277) Document the three frame sampling modes and how the SDK handles the universal parameter automatically via context variable. Notes upcoming property renames per mmif#238. --- documentation/tutorial.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/documentation/tutorial.md b/documentation/tutorial.md index 5c480b7..b7f7469 100644 --- a/documentation/tutorial.md +++ b/documentation/tutorial.md @@ -226,6 +226,27 @@ def _run_nlp_tool(self, doc, new_view): First, with `text_value` we get the text from the text document, either from its `location` property or from its `text` property. Second, we apply the tokenizer to the text. And third, we loop over the token offsets in the tokenizer result and create annotations of type `Uri.TOKEN` with an identifier that is automatically generated by the SDK. All that is needed for adding an annotation is the `new_annotation()` method on the view object and the `add_property()` method on the annotation object. +## Working with TimeFrame Annotations + +Many CLAMS apps process video by operating on TimeFrame annotations produced by an upstream app (e.g., scene detection, shot segmentation). A TimeFrame can carry structural members (currently called `targets` — a list of TimePoint IDs covering every frame in the segment), a salient subset of those members (currently called `representatives`), or simply `start`/`end` boundaries. + +> **Note** +> The property names `targets` and `representatives` are under review and may be renamed in a future MMIF spec version. See [mmif#238](https://github.com/clamsproject/mmif/issues/238) for the ongoing discussion. The SDK API will be updated accordingly. + +### Frame sampling with `tfSamplingMode` + +When your app receives TimeFrame annotations, the caller can control which frames your app processes by setting the `tfSamplingMode` runtime parameter. This is a **universal parameter** — automatically available on every CLAMS app without any per-app configuration. + +There are three modes: + +- `representatives` (default) — use the frames listed in the TimeFrame's `representatives` property. If no representatives exist, the TimeFrame is skipped. +- `single` — pick one frame: the middle representative if available, otherwise the midpoint of the start/end interval. +- `all` — use every frame in `targets` if present, otherwise generate every frame in the start/end interval. + +App developers do **not** need to handle this parameter themselves. The SDK intercepts it in `annotate()` and sets a context variable before `_annotate()` runs. Inside `_annotate()`, calls to `vdh.extract_frames_by_mode()` automatically read the active mode and select frames accordingly. The underlying per-mode functions (`_sample_representatives()`, `_sample_single()`, `_sample_all()`) in `mmif.utils.video_document_helper` are also available for apps that need frame numbers without extracting images. + +See the `extract_frames_by_mode()` API documentation for details. + ## Containerization with Docker Apps within CLAMS typically run as Flask servers in Docker containers, and after an app is tested as a local Flask application, it should be containerized. In fact, in some cases we don't even bother running a local Flask server and move straight to the container set up. From 25e6c009537fa047897bd8ede352e06498123493 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Wed, 11 Mar 2026 20:40:28 -0400 Subject: [PATCH 9/9] Add hf4/hf5 container images, keep hf as alias to hf5 (#256) Split -hf images into -hf4 (transformers 4.x) and -hf5 (transformers 5.x) variants. Existing -hf images become thin aliases that FROM the corresponding -hf5 image for backward compatibility. --- .github/workflows/containers.yml | 64 ++++++++++++++++++++++++++--- container/ffmpeg-hf.containerfile | 8 +--- container/ffmpeg-hf4.containerfile | 6 +++ container/ffmpeg-hf5.containerfile | 6 +++ container/hf.containerfile | 8 +--- container/hf4.containerfile | 6 +++ container/hf5.containerfile | 6 +++ container/opencv4-hf.containerfile | 8 +--- container/opencv4-hf4.containerfile | 6 +++ container/opencv4-hf5.containerfile | 6 +++ 10 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 container/ffmpeg-hf4.containerfile create mode 100644 container/ffmpeg-hf5.containerfile create mode 100644 container/hf4.containerfile create mode 100644 container/hf5.containerfile create mode 100644 container/opencv4-hf4.containerfile create mode 100644 container/opencv4-hf5.containerfile diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 2c9160c..9241041 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -116,6 +116,15 @@ jobs: buildfilename: './container/opencv4.containerfile' version: ${{ needs.set-version.outputs.version }} + call-build-hf: + name: "🤙 Call container workflow with `hf`" + needs: ['set-version', 'call-build-hf5'] + uses: ./.github/workflows/container.yml + secrets: inherit + with: + buildfilename: './container/hf.containerfile' + version: ${{ needs.set-version.outputs.version }} + call-build-opencv4-tf2: name: "🤙 Call container workflow with `opencv4-tf2`" needs: ['set-version', 'call-build-opencv4'] @@ -134,29 +143,74 @@ jobs: buildfilename: './container/opencv4-torch2.containerfile' version: ${{ needs.set-version.outputs.version }} - call-build-hf: - name: "🤙 Call container workflow with `hf`" + call-build-hf4: + name: "🤙 Call container workflow with `hf4`" needs: ['set-version', 'call-build-torch2'] uses: ./.github/workflows/container.yml secrets: inherit with: - buildfilename: './container/hf.containerfile' + buildfilename: './container/hf4.containerfile' + version: ${{ needs.set-version.outputs.version }} + + call-build-hf5: + name: "🤙 Call container workflow with `hf5`" + needs: ['set-version', 'call-build-torch2'] + uses: ./.github/workflows/container.yml + secrets: inherit + with: + buildfilename: './container/hf5.containerfile' version: ${{ needs.set-version.outputs.version }} call-build-ffmpeg-hf: name: "🤙 Call container workflow with `ffmpeg-hf`" - needs: ['set-version', 'call-build-ffmpeg-torch2'] + needs: ['set-version', 'call-build-ffmpeg-hf5'] uses: ./.github/workflows/container.yml secrets: inherit with: buildfilename: './container/ffmpeg-hf.containerfile' version: ${{ needs.set-version.outputs.version }} + call-build-ffmpeg-hf4: + name: "🤙 Call container workflow with `ffmpeg-hf4`" + needs: ['set-version', 'call-build-ffmpeg-torch2'] + uses: ./.github/workflows/container.yml + secrets: inherit + with: + buildfilename: './container/ffmpeg-hf4.containerfile' + version: ${{ needs.set-version.outputs.version }} + + call-build-ffmpeg-hf5: + name: "🤙 Call container workflow with `ffmpeg-hf5`" + needs: ['set-version', 'call-build-ffmpeg-torch2'] + uses: ./.github/workflows/container.yml + secrets: inherit + with: + buildfilename: './container/ffmpeg-hf5.containerfile' + version: ${{ needs.set-version.outputs.version }} + call-build-opencv4-hf: name: "🤙 Call container workflow with `opencv4-hf`" - needs: ['set-version', 'call-build-opencv4-torch2'] + needs: ['set-version', 'call-build-opencv4-hf5'] uses: ./.github/workflows/container.yml secrets: inherit with: buildfilename: './container/opencv4-hf.containerfile' version: ${{ needs.set-version.outputs.version }} + + call-build-opencv4-hf4: + name: "🤙 Call container workflow with `opencv4-hf4`" + needs: ['set-version', 'call-build-opencv4-torch2'] + uses: ./.github/workflows/container.yml + secrets: inherit + with: + buildfilename: './container/opencv4-hf4.containerfile' + version: ${{ needs.set-version.outputs.version }} + + call-build-opencv4-hf5: + name: "🤙 Call container workflow with `opencv4-hf5`" + needs: ['set-version', 'call-build-opencv4-torch2'] + uses: ./.github/workflows/container.yml + secrets: inherit + with: + buildfilename: './container/opencv4-hf5.containerfile' + version: ${{ needs.set-version.outputs.version }} diff --git a/container/ffmpeg-hf.containerfile b/container/ffmpeg-hf.containerfile index 0a0dc6b..2a5f387 100644 --- a/container/ffmpeg-hf.containerfile +++ b/container/ffmpeg-hf.containerfile @@ -1,7 +1,3 @@ ARG clams_version -FROM ghcr.io/clamsproject/clams-python-ffmpeg-torch2:$clams_version -LABEL org.opencontainers.image.description="clams-python-ffmpeg-hf image is shipped with clams-python, ffmpeg, and vairous huggingface libraries (PyTorch backend)" - -RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* -RUN pip install --no-cache-dir datasets - +FROM ghcr.io/clamsproject/clams-python-ffmpeg-hf5:$clams_version +LABEL org.opencontainers.image.description="clams-python-ffmpeg-hf image is an alias for clams-python-ffmpeg-hf5 (latest HuggingFace/transformers)" diff --git a/container/ffmpeg-hf4.containerfile b/container/ffmpeg-hf4.containerfile new file mode 100644 index 0000000..22f70cd --- /dev/null +++ b/container/ffmpeg-hf4.containerfile @@ -0,0 +1,6 @@ +ARG clams_version +FROM ghcr.io/clamsproject/clams-python-ffmpeg-torch2:$clams_version +LABEL org.opencontainers.image.description="clams-python-ffmpeg-hf4 image is shipped with clams-python, ffmpeg, and HuggingFace libraries with transformers 4.x (PyTorch backend)" + +RUN pip install --no-cache-dir transformers[torch,tokenizers]==4.* +RUN pip install --no-cache-dir datasets diff --git a/container/ffmpeg-hf5.containerfile b/container/ffmpeg-hf5.containerfile new file mode 100644 index 0000000..ceb6e4c --- /dev/null +++ b/container/ffmpeg-hf5.containerfile @@ -0,0 +1,6 @@ +ARG clams_version +FROM ghcr.io/clamsproject/clams-python-ffmpeg-torch2:$clams_version +LABEL org.opencontainers.image.description="clams-python-ffmpeg-hf5 image is shipped with clams-python, ffmpeg, and HuggingFace libraries with transformers 5.x (PyTorch backend)" + +RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* +RUN pip install --no-cache-dir datasets diff --git a/container/hf.containerfile b/container/hf.containerfile index 77c9d8e..6f7bf99 100644 --- a/container/hf.containerfile +++ b/container/hf.containerfile @@ -1,7 +1,3 @@ ARG clams_version -FROM ghcr.io/clamsproject/clams-python-torch2:$clams_version -LABEL org.opencontainers.image.description="clams-python-hf image is shipped with clams-python and vairous huggingface libraries (PyTorch backend)" - -RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* -RUN pip install --no-cache-dir datasets - +FROM ghcr.io/clamsproject/clams-python-hf5:$clams_version +LABEL org.opencontainers.image.description="clams-python-hf image is an alias for clams-python-hf5 (latest HuggingFace/transformers)" diff --git a/container/hf4.containerfile b/container/hf4.containerfile new file mode 100644 index 0000000..73b1a78 --- /dev/null +++ b/container/hf4.containerfile @@ -0,0 +1,6 @@ +ARG clams_version +FROM ghcr.io/clamsproject/clams-python-torch2:$clams_version +LABEL org.opencontainers.image.description="clams-python-hf4 image is shipped with clams-python and HuggingFace libraries with transformers 4.x (PyTorch backend)" + +RUN pip install --no-cache-dir transformers[torch,tokenizers]==4.* +RUN pip install --no-cache-dir datasets diff --git a/container/hf5.containerfile b/container/hf5.containerfile new file mode 100644 index 0000000..71e0e30 --- /dev/null +++ b/container/hf5.containerfile @@ -0,0 +1,6 @@ +ARG clams_version +FROM ghcr.io/clamsproject/clams-python-torch2:$clams_version +LABEL org.opencontainers.image.description="clams-python-hf5 image is shipped with clams-python and HuggingFace libraries with transformers 5.x (PyTorch backend)" + +RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* +RUN pip install --no-cache-dir datasets diff --git a/container/opencv4-hf.containerfile b/container/opencv4-hf.containerfile index a1b4841..15226ba 100644 --- a/container/opencv4-hf.containerfile +++ b/container/opencv4-hf.containerfile @@ -1,7 +1,3 @@ ARG clams_version -FROM ghcr.io/clamsproject/clams-python-opencv4-torch2:$clams_version -LABEL org.opencontainers.image.description="clams-python-opencv4-hf image is shipped with clams-python, opencv4 (ffmpeg backend), and vairous huggingface libraries (PyTorch backend)" - -RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* -RUN pip install --no-cache-dir datasets - +FROM ghcr.io/clamsproject/clams-python-opencv4-hf5:$clams_version +LABEL org.opencontainers.image.description="clams-python-opencv4-hf image is an alias for clams-python-opencv4-hf5 (latest HuggingFace/transformers)" diff --git a/container/opencv4-hf4.containerfile b/container/opencv4-hf4.containerfile new file mode 100644 index 0000000..a79a164 --- /dev/null +++ b/container/opencv4-hf4.containerfile @@ -0,0 +1,6 @@ +ARG clams_version +FROM ghcr.io/clamsproject/clams-python-opencv4-torch2:$clams_version +LABEL org.opencontainers.image.description="clams-python-opencv4-hf4 image is shipped with clams-python, opencv4 (ffmpeg backend), and HuggingFace libraries with transformers 4.x (PyTorch backend)" + +RUN pip install --no-cache-dir transformers[torch,tokenizers]==4.* +RUN pip install --no-cache-dir datasets diff --git a/container/opencv4-hf5.containerfile b/container/opencv4-hf5.containerfile new file mode 100644 index 0000000..bca8759 --- /dev/null +++ b/container/opencv4-hf5.containerfile @@ -0,0 +1,6 @@ +ARG clams_version +FROM ghcr.io/clamsproject/clams-python-opencv4-torch2:$clams_version +LABEL org.opencontainers.image.description="clams-python-opencv4-hf5 image is shipped with clams-python, opencv4 (ffmpeg backend), and HuggingFace libraries with transformers 5.x (PyTorch backend)" + +RUN pip install --no-cache-dir transformers[torch,tokenizers]==5.* +RUN pip install --no-cache-dir datasets