clams-python 1.7.4__tar.gz → 1.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {clams_python-1.7.4 → clams_python-1.8.0}/.gitignore +1 -1
  2. {clams_python-1.7.4 → clams_python-1.8.0}/CHANGELOG.md +13 -0
  3. {clams_python-1.7.4 → clams_python-1.8.0}/CONTRIBUTING.md +1 -1
  4. {clams_python-1.7.4/clams_python.egg-info → clams_python-1.8.0}/PKG-INFO +4 -4
  5. {clams_python-1.7.4 → clams_python-1.8.0}/build-tools/clean.py +1 -1
  6. {clams_python-1.7.4 → clams_python-1.8.0}/build-tools/test.py +4 -4
  7. {clams_python-1.7.4 → clams_python-1.8.0}/clams/__init__.py +1 -1
  8. {clams_python-1.7.4 → clams_python-1.8.0}/clams/app/__init__.py +257 -39
  9. {clams_python-1.7.4 → clams_python-1.8.0}/clams/appmetadata/__init__.py +18 -4
  10. {clams_python-1.7.4 → clams_python-1.8.0}/clams/backends/hf.py +4 -4
  11. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/__init__.py +1 -1
  12. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/Containerfile.template +0 -14
  13. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/utl-tf/timeframe.py.template +40 -21
  14. {clams_python-1.7.4 → clams_python-1.8.0}/clams/envelop/__init__.py +1 -1
  15. {clams_python-1.7.4 → clams_python-1.8.0/clams_python.egg-info}/PKG-INFO +4 -4
  16. {clams_python-1.7.4 → clams_python-1.8.0}/clams_python.egg-info/SOURCES.txt +1 -1
  17. {clams_python-1.7.4 → clams_python-1.8.0}/clams_python.egg-info/requires.txt +3 -3
  18. {clams_python-1.7.4 → clams_python-1.8.0}/clams_python.egg-info/scm_file_list.json +1 -1
  19. clams_python-1.8.0/clams_python.egg-info/scm_version.json +8 -0
  20. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/app-baseclasses.rst +65 -0
  21. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/target-versions.csv +1 -0
  22. {clams_python-1.7.4 → clams_python-1.8.0}/pyproject.toml +18 -3
  23. clams_python-1.8.0/tests/test_develop.py +122 -0
  24. {clams_python-1.7.4 → clams_python-1.8.0}/tests/test_promptable.py +261 -0
  25. clams_python-1.7.4/.pytype.cfg +0 -10
  26. clams_python-1.7.4/clams_python.egg-info/scm_version.json +0 -8
  27. {clams_python-1.7.4 → clams_python-1.8.0}/.github/workflows/codecov.yml +0 -0
  28. {clams_python-1.7.4 → clams_python-1.8.0}/.github/workflows/container.yml +0 -0
  29. {clams_python-1.7.4 → clams_python-1.8.0}/.github/workflows/containers.yml +0 -0
  30. {clams_python-1.7.4 → clams_python-1.8.0}/.github/workflows/issue-assign.yml +0 -0
  31. {clams_python-1.7.4 → clams_python-1.8.0}/.github/workflows/issue-close.yml +0 -0
  32. {clams_python-1.7.4 → clams_python-1.8.0}/.github/workflows/issue-infra-project.yml +0 -0
  33. {clams_python-1.7.4 → clams_python-1.8.0}/.github/workflows/publish.yml +0 -0
  34. {clams_python-1.7.4 → clams_python-1.8.0}/.github/workflows/release-check.yml +0 -0
  35. {clams_python-1.7.4 → clams_python-1.8.0}/LICENSE +0 -0
  36. {clams_python-1.7.4 → clams_python-1.8.0}/README.md +0 -0
  37. {clams_python-1.7.4 → clams_python-1.8.0}/build-tools/build.py +0 -0
  38. {clams_python-1.7.4 → clams_python-1.8.0}/build-tools/docs.py +0 -0
  39. {clams_python-1.7.4 → clams_python-1.8.0}/build-tools/prep_release.py +0 -0
  40. {clams_python-1.7.4 → clams_python-1.8.0}/build-tools/publish.py +0 -0
  41. {clams_python-1.7.4 → clams_python-1.8.0}/clams/backends/__init__.py +0 -0
  42. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/.dockerignore.template +0 -0
  43. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/.gitignore.template +0 -0
  44. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/LICENSE.template +0 -0
  45. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/README.md.template +0 -0
  46. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/app.py.template +0 -0
  47. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/cli.py.template +0 -0
  48. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/metadata.py.template +0 -0
  49. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/app/requirements.txt.template +0 -0
  50. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/gha/for-clams-team.md.template +0 -0
  51. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/gha/workflows/issue-apps-project.yml.template +0 -0
  52. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/gha/workflows/issue-assign.yml.template +0 -0
  53. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/gha/workflows/issue-close.yml.template +0 -0
  54. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/gha/workflows/publish.yml.template +0 -0
  55. {clams_python-1.7.4 → clams_python-1.8.0}/clams/develop/templates/utl-tf/__init__.py.template +0 -0
  56. {clams_python-1.7.4 → clams_python-1.8.0}/clams/restify/__init__.py +0 -0
  57. {clams_python-1.7.4 → clams_python-1.8.0}/clams/serve/__init__.py +0 -0
  58. {clams_python-1.7.4 → clams_python-1.8.0}/clams/ver/__init__.py +0 -0
  59. {clams_python-1.7.4 → clams_python-1.8.0}/clams_python.egg-info/dependency_links.txt +0 -0
  60. {clams_python-1.7.4 → clams_python-1.8.0}/clams_python.egg-info/entry_points.txt +0 -0
  61. {clams_python-1.7.4 → clams_python-1.8.0}/clams_python.egg-info/top_level.txt +0 -0
  62. {clams_python-1.7.4 → clams_python-1.8.0}/container/Containerfile +0 -0
  63. {clams_python-1.7.4 → clams_python-1.8.0}/container/ffmpeg-hf.containerfile +0 -0
  64. {clams_python-1.7.4 → clams_python-1.8.0}/container/ffmpeg-hf4.containerfile +0 -0
  65. {clams_python-1.7.4 → clams_python-1.8.0}/container/ffmpeg-hf5.containerfile +0 -0
  66. {clams_python-1.7.4 → clams_python-1.8.0}/container/ffmpeg-tf2.containerfile +0 -0
  67. {clams_python-1.7.4 → clams_python-1.8.0}/container/ffmpeg-torch2.containerfile +0 -0
  68. {clams_python-1.7.4 → clams_python-1.8.0}/container/ffmpeg.containerfile +0 -0
  69. {clams_python-1.7.4 → clams_python-1.8.0}/container/generate_containers_yml.py +0 -0
  70. {clams_python-1.7.4 → clams_python-1.8.0}/container/hf.containerfile +0 -0
  71. {clams_python-1.7.4 → clams_python-1.8.0}/container/hf4.containerfile +0 -0
  72. {clams_python-1.7.4 → clams_python-1.8.0}/container/hf5.containerfile +0 -0
  73. {clams_python-1.7.4 → clams_python-1.8.0}/container/jdk8.containerfile +0 -0
  74. {clams_python-1.7.4 → clams_python-1.8.0}/container/opencv4-hf.containerfile +0 -0
  75. {clams_python-1.7.4 → clams_python-1.8.0}/container/opencv4-hf4.containerfile +0 -0
  76. {clams_python-1.7.4 → clams_python-1.8.0}/container/opencv4-hf5.containerfile +0 -0
  77. {clams_python-1.7.4 → clams_python-1.8.0}/container/opencv4-tf2.containerfile +0 -0
  78. {clams_python-1.7.4 → clams_python-1.8.0}/container/opencv4-torch2.containerfile +0 -0
  79. {clams_python-1.7.4 → clams_python-1.8.0}/container/opencv4.containerfile +0 -0
  80. {clams_python-1.7.4 → clams_python-1.8.0}/container/tf2.containerfile +0 -0
  81. {clams_python-1.7.4 → clams_python-1.8.0}/container/torch2.containerfile +0 -0
  82. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/appdirectory.rst +0 -0
  83. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/appmetadata.rst +0 -0
  84. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/autodoc/clams.app.rst +0 -0
  85. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/autodoc/clams.appmetadata.rst +0 -0
  86. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/autodoc/clams.backends.rst +0 -0
  87. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/autodoc/clams.mmif_utils.rst +0 -0
  88. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/autodoc/clams.restify.rst +0 -0
  89. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/clamsapp.rst +0 -0
  90. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/cli.rst +0 -0
  91. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/conf.py +0 -0
  92. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/gpu-apps.rst +0 -0
  93. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/index.rst +0 -0
  94. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/input-output.rst +0 -0
  95. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/introduction.rst +0 -0
  96. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/modules.rst +0 -0
  97. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/runtime-params.rst +0 -0
  98. {clams_python-1.7.4 → clams_python-1.8.0}/documentation/tutorial.md +0 -0
  99. {clams_python-1.7.4 → clams_python-1.8.0}/setup.cfg +0 -0
  100. {clams_python-1.7.4 → clams_python-1.8.0}/tests/metadata.py +0 -0
  101. {clams_python-1.7.4 → clams_python-1.8.0}/tests/pillow-logo.png +0 -0
  102. {clams_python-1.7.4 → clams_python-1.8.0}/tests/test_backends_hf.py +0 -0
  103. {clams_python-1.7.4 → clams_python-1.8.0}/tests/test_clamsapp.py +0 -0
  104. {clams_python-1.7.4 → clams_python-1.8.0}/tests/test_envelope.py +0 -0
@@ -1,6 +1,6 @@
1
1
  # python test files
2
2
  .coverage
3
- .pytype
3
+ .mypy_cache
4
4
 
5
5
  # build time temp files
6
6
  VERSION*
@@ -1,4 +1,17 @@
1
1
 
2
+ ## releasing 1.7.4 (2026-09-01)
3
+ ### Overview
4
+
5
+ Fixes parameter recording in signed view metadata, adds consistent handling of empty views, and repairs the `-torch` base container images.
6
+
7
+ ### Changes
8
+
9
+ * `parameters`: fix recording of multi-valued and raw parameters when signing a view, and clarify the distinction between `params` and `appConfiguration` in view metadata (https://github.com/clamsproject/clams-python/issues/296)
10
+ * views: handle "empty" views returned from apps consistently and emit an SDK-level warning when an app returns no new annotations (https://github.com/clamsproject/clams-python/issues/305)
11
+ * `-torch` container images: add a minimal C tool chain to the base images for triton compatibility ( https://github.com/clamsproject/clams-python/issues/302)
12
+
13
+
14
+
2
15
  ## releasing 1.7.3 (2026-07-09)
3
16
  ### Overview
4
17
  This patch includes critical bugfixes. See changes below.
@@ -15,7 +15,7 @@ Unlike the old `setup.py`-based workflow, an editable install
15
15
  (`pip install -e .`) is now required before running tests or building
16
16
  docs. The package uses `importlib.metadata` for version resolution at
17
17
  runtime, which only works when the package is registered in the
18
- environment. You can no longer run `pytest` or `pytype` directly
18
+ environment. You can no longer run `pytest` or `mypy` directly
19
19
  against the source tree without installing first. If you want to avoid
20
20
  pulling in all dependencies, `pip install -e . --no-deps` is sufficient
21
21
  to register the package metadata.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clams-python
3
- Version: 1.7.4
3
+ Version: 1.8.0
4
4
  Summary: A collection of APIs to develop CLAMS app for python
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://clams.ai
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: mmif-python==1.5.3
16
+ Requires-Dist: mmif-python==1.5.4
17
17
  Requires-Dist: Flask>=2
18
18
  Requires-Dist: Flask-RESTful>=0.3.9
19
19
  Requires-Dist: gunicorn>=20
@@ -21,7 +21,7 @@ Requires-Dist: lapps>=0.0.2
21
21
  Requires-Dist: pydantic>=2
22
22
  Requires-Dist: jsonschema>=3
23
23
  Provides-Extra: dev
24
- Requires-Dist: pytype; extra == "dev"
24
+ Requires-Dist: mypy; extra == "dev"
25
25
  Requires-Dist: pytest; extra == "dev"
26
26
  Requires-Dist: pytest-cov; extra == "dev"
27
27
  Requires-Dist: pillow; extra == "dev"
@@ -32,7 +32,7 @@ Requires-Dist: furo; extra == "docs"
32
32
  Requires-Dist: m2r2; extra == "docs"
33
33
  Requires-Dist: sphinx-jsonschema; extra == "docs"
34
34
  Provides-Extra: test
35
- Requires-Dist: pytype; extra == "test"
35
+ Requires-Dist: mypy; extra == "test"
36
36
  Requires-Dist: pytest; extra == "test"
37
37
  Requires-Dist: pytest-cov; extra == "test"
38
38
  Requires-Dist: pillow; extra == "test"
@@ -13,7 +13,7 @@ PROJECT_ROOT = SCRIPT_DIR.parent
13
13
  # Directories to remove
14
14
  CLEAN_DIRS = [
15
15
  "build", "dist", "*.egg-info", "clams_python-*",
16
- ".pytest_cache", ".pytype", ".hypothesis",
16
+ ".pytest_cache", ".mypy_cache", ".hypothesis",
17
17
  "tests/.hypothesis", "htmlcov",
18
18
  "docs-test",
19
19
  ]
@@ -3,7 +3,7 @@ Run tests for the clams-python package.
3
3
 
4
4
  This script is equivalent to ``make test`` in the Makefile-based repos:
5
5
  pip install -e ".[test]"
6
- pytype --config .pytype.cfg clams
6
+ python -m mypy
7
7
  python -m pytest --cov=clams --cov-report=xml
8
8
  """
9
9
  import argparse
@@ -50,10 +50,10 @@ def main():
50
50
  cwd=project_root,
51
51
  )
52
52
 
53
- # Run pytype static analysis
54
- print("\n--- Running pytype ---")
53
+ # Run mypy static analysis (configured in pyproject.toml)
54
+ print("\n--- Running mypy ---")
55
55
  run_command(
56
- ["pytype", "--config", ".pytype.cfg", "clams"],
56
+ [sys.executable, "-m", "mypy"],
57
57
  cwd=project_root,
58
58
  )
59
59
 
@@ -11,7 +11,7 @@ from clams.envelop import create_envelope
11
11
  from clams.restify import Restifier
12
12
  from clams.ver import __version__
13
13
 
14
- __all__ = [AppMetadata, Restifier] + app_all
14
+ __all__ = ['AppMetadata', 'Restifier'] + app_all
15
15
  version_template = "{} (based on MMIF spec: {})"
16
16
 
17
17
 
@@ -1,3 +1,4 @@
1
+ import inspect
1
2
  import json
2
3
  import logging
3
4
  import os
@@ -6,19 +7,22 @@ import sys
6
7
  import warnings
7
8
  from abc import ABC, abstractmethod
8
9
  from contextlib import contextmanager
10
+ from dataclasses import dataclass
11
+ from enum import Enum
9
12
  from datetime import datetime
10
13
  from urllib import parse as urlparser
11
14
 
12
- __all__ = ['ClamsApp', 'ClamsPromptableApp', 'ClamsHFPromptableApp']
15
+ __all__ = ['ClamsApp', 'ClamsPromptableApp', 'ClamsHFPromptableApp',
16
+ 'PromptTask', 'ImagesPerPromptMode']
13
17
 
14
- from typing import Union, Any, Optional, Dict, List, Tuple, cast
18
+ from typing import Union, Any, Optional, Dict, List, Mapping, Tuple, cast
15
19
 
16
20
  from mmif import Mmif, Document, DocumentTypes, View, AnnotationTypes
17
21
  from mmif.utils.video_document_helper import (
18
22
  SamplingMode, SAMPLING_MODE_DESCRIPTIONS, SAMPLING_MODE_DEFAULT,
19
23
  _sampling_mode,
20
24
  )
21
- from mmif.utils.workflow_helper import generate_param_hash # pytype: disable=import-error
25
+ from mmif.utils.workflow_helper import generate_param_hash
22
26
  from clams.appmetadata import AppMetadata, real_valued_primitives, python_type, map_param_kv_delimiter
23
27
  from clams.envelop import unwrap_if_envelope
24
28
 
@@ -53,6 +57,47 @@ class EmptyOutputWarning(UserWarning):
53
57
  """Raised when an app's ``_annotate()`` produces no non-empty views."""
54
58
 
55
59
 
60
+ class ImagesPerPromptMode(Enum):
61
+ """
62
+ How :py:meth:`ClamsPromptableApp.chunk_tasks` sizes the chunks of a
63
+ task that has more images than the cap. Both modes produce the same
64
+ number of chunks; they differ in how images are spread over them.
65
+ """
66
+ #: Fill each chunk to the cap; the last chunk holds the remainder.
67
+ MAX = 'max'
68
+ #: Spread images evenly, so chunk sizes differ by at most one.
69
+ BALANCED = 'balanced'
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class PromptTask:
74
+ """
75
+ One unit of promptable work: the images bundled into a single prompt,
76
+ the annotations they were drawn from, and the annotation the response
77
+ is anchored to.
78
+
79
+ ``images`` and ``origins`` are parallel: ``origins[i]`` is the ``id``
80
+ of the annotation (typically a ``TimePoint``) that ``images[i]`` was
81
+ extracted from. ``source`` is the ``id`` of the coarse anchor
82
+ (typically the parent ``TimeFrame``). The fields map directly onto
83
+ :py:meth:`ClamsPromptableApp.response_to_grounded_textdocument`.
84
+ ``source`` also serves as the join key for app-side data about the
85
+ anchor, such as the TimeFrame label.
86
+
87
+ :raises ValueError: if ``images`` and ``origins`` differ in length.
88
+ """
89
+ images: List[Any]
90
+ origins: List[str]
91
+ source: str
92
+
93
+ def __post_init__(self):
94
+ if len(self.images) != len(self.origins):
95
+ raise ValueError(
96
+ f"PromptTask images and origins must be parallel; got "
97
+ f"{len(self.images)} images and {len(self.origins)} "
98
+ f"origins for source {self.source!r}.")
99
+
100
+
56
101
  class ClamsApp(ABC):
57
102
  """
58
103
  An abstract class to define API's for ClamsApps. A CLAMS app should inherit
@@ -62,7 +107,7 @@ class ClamsApp(ABC):
62
107
  # A set of "universal runtime parameters that can be used for both GET and POST anytime".
63
108
  # The behavioral changes based on these parameters must be implemented on the SDK level.
64
109
  # This needs to stay as a list of dicts for compatibility with the metadata.py file (templated).
65
- universal_parameters = [
110
+ universal_parameters: List[Dict[str, Any]] = [
66
111
  {
67
112
  'name': 'pretty', 'type': 'boolean', 'choices': None, 'default': False, 'multivalued': False,
68
113
  'description': 'The JSON body of the HTTP response will be re-formatted with 2-space indentation',
@@ -137,10 +182,10 @@ class ClamsApp(ABC):
137
182
  For metadata specification,
138
183
  see `https://clams.ai/clams-python/appmetadata.jsonschema <../appmetadata.jsonschema>`_.
139
184
  """
140
- cwd = pathlib.Path(sys.modules[self.__module__].__file__).parent
185
+ cwd = pathlib.Path(inspect.getfile(type(self))).parent
141
186
 
142
187
  if (cwd / 'metadata.py').exists():
143
- import metadata as metadatapy # pytype: disable=import-error
188
+ import metadata as metadatapy
144
189
  metadata = metadatapy.appmetadata()
145
190
  else:
146
191
  metadata = self._appmetadata()
@@ -180,7 +225,7 @@ class ClamsApp(ABC):
180
225
  mmif, runtime_params = unwrap_if_envelope(mmif, runtime_params)
181
226
  mmif = Mmif(mmif)
182
227
  existing_view_ids = {view.id for view in mmif.views}
183
- issued_warnings = []
228
+ issued_warnings: List[Union[Warning, warnings.WarningMessage]] = []
184
229
  for key in runtime_params:
185
230
  if key not in self.annotate_param_spec:
186
231
  issued_warnings.append(UserWarning(f'An undefined parameter "{key}" (value: "{runtime_params[key]}") is passed'))
@@ -214,7 +259,7 @@ class ClamsApp(ABC):
214
259
  td = run_id - t
215
260
  runningTime = refined.get('runningTime', False)
216
261
  hwFetch = refined.get('hwFetch', False)
217
- runtime_recs = {}
262
+ runtime_recs: Dict[str, Any] = {}
218
263
  if hwFetch:
219
264
  import multiprocessing
220
265
  import platform, shutil, subprocess
@@ -237,7 +282,7 @@ class ClamsApp(ABC):
237
282
  for annotated_view in annotated.views:
238
283
  if annotated_view.id not in existing_view_ids and annotated_view.metadata.app == str(self.metadata.identifier):
239
284
  annotated_view.metadata.timestamp = run_id
240
- profiling_data = {}
285
+ profiling_data: Dict[str, Any] = {}
241
286
  if runningTime:
242
287
  profiling_data['runningTime'] = str(td)
243
288
  if len(runtime_recs) > 0:
@@ -280,7 +325,7 @@ class ClamsApp(ABC):
280
325
  if self._RAW_PARAMS_KEY in runtime_params:
281
326
  # meaning the dict is already refined, just return it
282
327
  return runtime_params
283
- refined = {}
328
+ refined: Dict[str, Any] = {}
284
329
 
285
330
  casted = self.annotate_param_caster.cast(runtime_params)
286
331
  for parameter in self.metadata.parameters:
@@ -491,7 +536,7 @@ class ClamsApp(ABC):
491
536
 
492
537
  # Fallback to torch (only sees current process memory)
493
538
  try:
494
- import torch # pytype: disable=import-error
539
+ import torch
495
540
  if not torch.cuda.is_available():
496
541
  return 0
497
542
 
@@ -595,7 +640,7 @@ class ClamsApp(ABC):
595
640
  available_before = {}
596
641
 
597
642
  try:
598
- import torch # pytype: disable=import-error
643
+ import torch
599
644
  torch_available = True
600
645
  cuda_available = torch.cuda.is_available()
601
646
  device_count = torch.cuda.device_count()
@@ -656,11 +701,9 @@ class ClamsApp(ABC):
656
701
  }
657
702
 
658
703
  # Record peak memory for future requests (if GPU app)
659
- gpu_app = (
660
- hasattr(app_instance, 'metadata') and
661
- getattr(app_instance.metadata, 'est_gpu_mem_min', 0) > 0
662
- )
663
- if gpu_app and total_peak > 0:
704
+ app_metadata = getattr(app_instance, 'metadata', None)
705
+ gpu_app = getattr(app_metadata, 'est_gpu_mem_min', 0) > 0
706
+ if app_instance is not None and gpu_app and total_peak > 0:
664
707
  app_instance._record_vram_usage(kwargs, total_peak)
665
708
 
666
709
  return result, cuda_profiler
@@ -729,6 +772,11 @@ class ClamsPromptableApp(ClamsApp):
729
772
  parameter set to ``AppMetadata``
730
773
  * :py:meth:`build_conversation` : assembles a chat-template-compatible
731
774
  message list from a prompt plus optional images/audios
775
+ * :py:meth:`chunk_tasks` : caps images per prompt by splitting
776
+ :class:`PromptTask`\\ s (``maxImagesPerPrompt``)
777
+ * :py:meth:`generate_batched` : runs :py:meth:`generate` in batches
778
+ of at most ``parallelPrompts`` prompts, reporting through
779
+ :py:meth:`report_progress`
732
780
  * :py:meth:`response_to_grounded_textdocument` : persists a
733
781
  generated response into a view as ``TextDocument`` +
734
782
  ``Alignment`` (+ optional ``origins`` / ``origination``)
@@ -737,7 +785,7 @@ class ClamsPromptableApp(ClamsApp):
737
785
  #: SDK-managed runtime parameters injected into every promptable app.
738
786
  #: These names are reserved; apps cannot redeclare them with
739
787
  #: customized specs.
740
- promptable_parameters = [
788
+ promptable_parameters: List[Dict[str, Any]] = [
741
789
  {
742
790
  'name': 'prompt', 'type': 'string', 'multivalued': True,
743
791
  'description':
@@ -827,6 +875,38 @@ class ClamsPromptableApp(ClamsApp):
827
875
  'Keep at ``1`` on memory-tight setups; raise only when '
828
876
  'per-prompt content is small and bounded.',
829
877
  },
878
+ {
879
+ 'name': 'maxImagesPerPrompt', 'type': 'integer', 'default': 0,
880
+ 'description':
881
+ 'Maximum number of images the app bundles into a single '
882
+ 'prompt (one generation, one output TextDocument). ``0`` '
883
+ 'means no limit, so all images for a TimeFrame go into one '
884
+ 'prompt and yield one TextDocument. With a positive N, the '
885
+ 'app splits a TimeFrame whose frames exceed N into '
886
+ 'consecutive groups of at most N; each group produces its '
887
+ 'own TextDocument aligned to the same TimeFrame and grounded '
888
+ 'to its own source TimePoints via ``origins``. This bounds '
889
+ 'GPU memory for long dynamic scenes. The value is '
890
+ 'operator-set, so the number of output TextDocuments is '
891
+ 'deterministic and does not vary by GPU. Composes with '
892
+ '``parallelPrompts``, which batches independent prompts '
893
+ 'into one forward pass.',
894
+ },
895
+ {
896
+ 'name': 'imagesPerPromptMode', 'type': 'string',
897
+ 'choices': [m.value for m in ImagesPerPromptMode],
898
+ 'default': ImagesPerPromptMode.MAX.value,
899
+ 'description':
900
+ 'How the images of a TimeFrame that exceeds '
901
+ '``maxImagesPerPrompt`` are spread over its prompts. Has '
902
+ 'no effect when ``maxImagesPerPrompt`` is ``0``. Both '
903
+ 'choices produce the same number of prompts. "max" fills '
904
+ 'each prompt to the cap and puts the remainder in the '
905
+ 'last one (33 images at a cap of 32 give 32 and 1). '
906
+ '"balanced" spreads the images evenly, so prompt sizes '
907
+ 'differ by at most one (33 images at a cap of 32 give 17 '
908
+ 'and 16).',
909
+ },
830
910
  ]
831
911
 
832
912
  @staticmethod
@@ -978,7 +1058,7 @@ class ClamsPromptableApp(ClamsApp):
978
1058
  if isinstance(prompt, str):
979
1059
  prompts = [prompt]
980
1060
  else:
981
- prompts = list(prompt)
1061
+ prompts = cast(List[str], list(prompt))
982
1062
 
983
1063
  if len(prompts) == 1:
984
1064
  return self._build_single_turn(
@@ -1072,6 +1152,148 @@ class ClamsPromptableApp(ClamsApp):
1072
1152
  base.append({'role': 'assistant', 'content': None})
1073
1153
  return convs
1074
1154
 
1155
+ def generate_batched(
1156
+ self,
1157
+ prompt: List[str],
1158
+ system_prompt: str = '',
1159
+ images: Optional[List[List[Any]]] = None,
1160
+ audios: Optional[List[List[Any]]] = None,
1161
+ prompt_mode: str = 'turn-taking',
1162
+ parallel_prompts: int = 1,
1163
+ **generation_params,
1164
+ ) -> List[str]:
1165
+ """
1166
+ Run N prompts through :py:meth:`generate` in batches of at most
1167
+ ``parallel_prompts`` prompts per call, and return N outputs in
1168
+ input order. Apps typically pass the ``parallelPrompts`` runtime
1169
+ parameter as ``parallel_prompts``.
1170
+
1171
+ The arguments other than ``parallel_prompts`` have the same
1172
+ meaning as in :py:meth:`generate` and are forwarded to it. A
1173
+ text-only call (``images`` and ``audios`` both ``None``) is one
1174
+ prompt and runs as a single :py:meth:`generate` call.
1175
+
1176
+ Progress is reported through :py:meth:`report_progress`: once
1177
+ before the first batch and once after each batch.
1178
+
1179
+ :param parallel_prompts: maximum number of prompts per
1180
+ :py:meth:`generate` call.
1181
+ :returns: one output per prompt, in input order.
1182
+ :rtype: List[str]
1183
+ :raises ValueError: if ``parallel_prompts`` is less than 1, or if
1184
+ ``images`` and ``audios`` are both given with different outer
1185
+ lengths.
1186
+ """
1187
+ if parallel_prompts < 1:
1188
+ raise ValueError(
1189
+ f"parallel_prompts must be at least 1; "
1190
+ f"got {parallel_prompts}.")
1191
+ if images is not None and audios is not None \
1192
+ and len(images) != len(audios):
1193
+ raise ValueError(
1194
+ f"images and audios must have the same outer length "
1195
+ f"when both are given; got "
1196
+ f"{len(images)} vs {len(audios)}.")
1197
+ groups = images if images is not None else audios
1198
+ if groups is None:
1199
+ self.report_progress(0, 1)
1200
+ outputs = self.generate(
1201
+ prompt, system_prompt=system_prompt,
1202
+ prompt_mode=prompt_mode, **generation_params)
1203
+ self.report_progress(1, 1)
1204
+ return outputs
1205
+ total = len(groups)
1206
+ outputs = []
1207
+ self.report_progress(0, total)
1208
+ for start in range(0, total, parallel_prompts):
1209
+ end = start + parallel_prompts
1210
+ outputs.extend(self.generate(
1211
+ prompt, system_prompt=system_prompt,
1212
+ images=images[start:end] if images is not None else None,
1213
+ audios=audios[start:end] if audios is not None else None,
1214
+ prompt_mode=prompt_mode, **generation_params))
1215
+ self.report_progress(min(end, total), total)
1216
+ return outputs
1217
+
1218
+ def report_progress(self, done: int, total: int) -> None:
1219
+ """
1220
+ Receive generation progress from :py:meth:`generate_batched`,
1221
+ which calls this once with ``done=0`` before the first batch and
1222
+ once after each batch. The default implementation writes one
1223
+ INFO log line. Override to report differently (another log
1224
+ level or wording, a progress bar) or to stay silent.
1225
+
1226
+ The app instance serves all requests, so an override that keeps
1227
+ state on ``self`` between calls is shared by concurrent
1228
+ requests.
1229
+
1230
+ :param done: number of prompts generated so far.
1231
+ :param total: number of prompts in this
1232
+ :py:meth:`generate_batched` call.
1233
+ """
1234
+ self.logger.info(f"Generated {done}/{total} prompts")
1235
+
1236
+ @staticmethod
1237
+ def chunk_tasks(
1238
+ tasks: List[PromptTask],
1239
+ max_images: int,
1240
+ mode: Union[str, ImagesPerPromptMode] = ImagesPerPromptMode.MAX,
1241
+ ) -> List[PromptTask]:
1242
+ """
1243
+ Split each task with more than ``max_images`` images into
1244
+ consecutive tasks of at most ``max_images`` images, chunking
1245
+ ``origins`` in step and keeping ``source``. Tasks at or under the
1246
+ cap pass through unchanged, and task order is preserved. Apps
1247
+ typically pass the ``maxImagesPerPrompt`` and
1248
+ ``imagesPerPromptMode`` runtime parameters as ``max_images`` and
1249
+ ``mode``.
1250
+
1251
+ Each resulting task yields one response and one ``TextDocument``,
1252
+ so an over-cap TimeFrame produces several ``TextDocument``\\ s
1253
+ aligned to the same ``source``, each grounded to its own
1254
+ ``origins``.
1255
+
1256
+ :param tasks: the tasks to split.
1257
+ :param max_images: maximum number of images per task. ``0`` means
1258
+ no cap.
1259
+ :param mode: how the images of an over-cap task are spread over
1260
+ its chunks; see :class:`ImagesPerPromptMode`.
1261
+ :returns: the capped tasks, in order.
1262
+ :rtype: List[PromptTask]
1263
+ :raises ValueError: if ``max_images`` is negative or ``mode`` is
1264
+ not a valid :class:`ImagesPerPromptMode`.
1265
+ """
1266
+ mode = ImagesPerPromptMode(mode)
1267
+ if max_images < 0:
1268
+ raise ValueError(
1269
+ f"max_images must be 0 (no cap) or positive; "
1270
+ f"got {max_images}.")
1271
+ if max_images == 0:
1272
+ return list(tasks)
1273
+ capped: List[PromptTask] = []
1274
+ for task in tasks:
1275
+ n_images = len(task.images)
1276
+ if n_images <= max_images:
1277
+ capped.append(task)
1278
+ continue
1279
+ n_chunks = -(-n_images // max_images)
1280
+ if mode is ImagesPerPromptMode.BALANCED:
1281
+ size, n_larger = divmod(n_images, n_chunks)
1282
+ sizes = ([size + 1] * n_larger
1283
+ + [size] * (n_chunks - n_larger))
1284
+ else:
1285
+ sizes = [max_images] * (n_chunks - 1)
1286
+ sizes.append(n_images - sum(sizes))
1287
+ start = 0
1288
+ for size in sizes:
1289
+ end = start + size
1290
+ capped.append(PromptTask(
1291
+ images=task.images[start:end],
1292
+ origins=task.origins[start:end],
1293
+ source=task.source))
1294
+ start = end
1295
+ return capped
1296
+
1075
1297
  def response_to_grounded_textdocument(
1076
1298
  self,
1077
1299
  view: View,
@@ -1330,7 +1552,8 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1330
1552
  "non-empty dict (HF model id -> commit hash). Set "
1331
1553
  "it on the ``AppMetadata`` constructor call before "
1332
1554
  "invoking this helper.")
1333
- choices = list(analyzer_versions.keys())
1555
+ choices: List[real_valued_primitives] = list(
1556
+ analyzer_versions.keys())
1334
1557
  default = choices[0] if len(choices) == 1 else None
1335
1558
  metadata.add_parameter(
1336
1559
  name='model',
@@ -1438,7 +1661,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1438
1661
  model_id, _, revision = model_id_or_with_rev.rpartition('@')
1439
1662
  else:
1440
1663
  model_id = model_id_or_with_rev
1441
- revision = self.metadata.analyzer_versions[model_id]
1664
+ revision = (self.metadata.analyzer_versions or {})[model_id]
1442
1665
  cache_key = (model_id, revision)
1443
1666
  cached = self._model_cache.get(cache_key)
1444
1667
  if cached is not None:
@@ -1590,7 +1813,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1590
1813
  ignores them, so subclasses can pass through the full
1591
1814
  ``**parameters`` dict from ``_annotate`` without filtering.
1592
1815
  """
1593
- gen_kwargs = {'max_new_tokens': max_new_tokens}
1816
+ gen_kwargs: Dict[str, Any] = {'max_new_tokens': max_new_tokens}
1594
1817
  if temperature > 0:
1595
1818
  gen_kwargs.update({
1596
1819
  'do_sample': True,
@@ -1628,7 +1851,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1628
1851
 
1629
1852
  class ParameterCaster(object):
1630
1853
 
1631
- def __init__(self, param_spec: Dict[str, Tuple[str, bool]]):
1854
+ def __init__(self, param_spec: Mapping[str, Tuple[str, bool]]):
1632
1855
  """
1633
1856
  A helper class to convert parameters passed by HTTP query strings to
1634
1857
  proper python data types.
@@ -1657,7 +1880,7 @@ class ParameterCaster(object):
1657
1880
  With the third case, developers can further process the nested values into a more complex data types or
1658
1881
  structures, but that is not in the scope of this Caster class.
1659
1882
  """
1660
- casted = {}
1883
+ casted: Dict[str, Any] = {}
1661
1884
  for k, vs in args.items():
1662
1885
  assert isinstance(vs, list), f"Expected a list of values for key {k}, but got {vs} of type {type(vs)}"
1663
1886
  assert all(isinstance(v, str) for v in vs), f"Expected a list of strings for key {k}, but got {vs} of types {[type(v) for v in vs]}"
@@ -1665,29 +1888,24 @@ class ParameterCaster(object):
1665
1888
  valuetype, multivalued = self.param_spec[k]
1666
1889
  for v in vs:
1667
1890
  if multivalued or k not in casted: # effectively only keeps the first value for non-multi params
1891
+ value: Any = v
1668
1892
  if valuetype == bool:
1669
- v = self.bool_param(v)
1893
+ value = self.bool_param(v)
1670
1894
  elif valuetype == float:
1671
- v = self.float_param(v)
1895
+ value = self.float_param(v)
1672
1896
  elif valuetype == int:
1673
- v = self.int_param(v)
1897
+ value = self.int_param(v)
1674
1898
  elif valuetype == str:
1675
- v = self.str_param(v)
1899
+ value = self.str_param(v)
1676
1900
  elif valuetype == dict:
1677
- v = self.kv_param(v)
1901
+ value = self.kv_param(v)
1678
1902
  if multivalued:
1679
1903
  if valuetype == dict:
1680
- casted.setdefault(k, {}).update(v)
1904
+ casted.setdefault(k, {}).update(value)
1681
1905
  else:
1682
- # pytype will complain about the next line, but it is actually correct
1683
- # casted.setdefault(k, []).append(v)
1684
- # so doing it in a more explicit way
1685
- if k in casted and isinstance(casted[k], list):
1686
- casted[k].append(v)
1687
- else:
1688
- casted[k] = [v]
1906
+ casted.setdefault(k, []).append(value)
1689
1907
  else:
1690
- casted[k] = v
1908
+ casted[k] = value
1691
1909
  # when an empty value is passed (usually as a default value)
1692
1910
  # just add an empty list or dict as a placeholder
1693
1911
  # explicit check for `len == 0` is required to prevent
@@ -1704,7 +1922,7 @@ class ParameterCaster(object):
1704
1922
  casted[k] = vs[0]
1705
1923
  else:
1706
1924
  casted[k] = vs
1707
- return casted # pytype: disable=bad-return-type
1925
+ return casted
1708
1926
 
1709
1927
  @staticmethod
1710
1928
  def bool_param(value) -> bool:
@@ -1,10 +1,11 @@
1
+ import inspect
1
2
  import json
2
3
  import os
3
4
  import shutil
4
5
  import subprocess
5
6
  import sys
6
7
  from pathlib import Path
7
- from typing import Union, Dict, List, Optional, Literal, Any
8
+ from typing import Union, Dict, List, Optional, Literal, Any, get_args
8
9
 
9
10
  import mmif
10
11
  import pydantic
@@ -22,7 +23,7 @@ map_param_kv_delimiter = ':'
22
23
  # dict to map app parameter data types to Python data types
23
24
  python_type = {"boolean": bool, "number": float, "integer": int, "string": str, "map": dict}
24
25
 
25
- param_value_types_values = param_value_types.__args__ # pytype: disable=attribute-error
26
+ param_value_types_values = get_args(param_value_types)
26
27
  app_directory_baseurl = "http://apps.clams.ai"
27
28
 
28
29
 
@@ -42,7 +43,9 @@ def get_clams_pyver():
42
43
 
43
44
  def generate_app_version(cwd=None):
44
45
  gitcmd = shutil.which('git')
45
- gitdir = (Path(sys.modules["__main__"].__file__).parent.resolve() if cwd is None else Path(cwd)) / '.git'
46
+ if cwd is None:
47
+ cwd = Path(inspect.getfile(sys.modules["__main__"])).parent.resolve()
48
+ gitdir = Path(cwd) / '.git'
46
49
  if gitcmd is not None and gitdir.exists():
47
50
  try:
48
51
  proc = subprocess.run([gitcmd, '--git-dir', str(gitdir), 'describe', '--tags', '--always'],
@@ -60,6 +63,12 @@ def get_mmif_specver():
60
63
  return mmif.__specver__
61
64
 
62
65
 
66
+ # TODO (krim @ 10/05/26): in the 2.0.0 cycle, make the pydantic-specific
67
+ # helpers in this module private (underscore-prefixed), as they are not
68
+ # part of the developer-facing API: the schema adjusters (`pop_titles`,
69
+ # `jsonschema_versioning`) and the validators (`validate_properties`,
70
+ # `at_type_must_be_str`, `assign_versions`, `validate_gpu_memory`,
71
+ # `append_version`).
63
72
  def pop_titles(js):
64
73
  for prop in js.get('properties', {}).values():
65
74
  prop.pop('title', None)
@@ -70,6 +79,11 @@ def jsonschema_versioning(js):
70
79
  js['$comment'] = f"clams-python SDK {get_clams_pyver()} was used to generate this schema"
71
80
 
72
81
 
82
+ def _adjust_appmetadata_schema(js):
83
+ pop_titles(js)
84
+ jsonschema_versioning(js)
85
+
86
+
73
87
  class _BaseModel(pydantic.BaseModel):
74
88
 
75
89
  model_config = {
@@ -381,7 +395,7 @@ class AppMetadata(pydantic.BaseModel):
381
395
  'title': 'CLAMS AppMetadata',
382
396
  'extra': 'forbid',
383
397
  'validate_by_name': True,
384
- 'json_schema_extra': lambda schema, model: [adjust(schema) for adjust in [pop_titles, jsonschema_versioning]],
398
+ 'json_schema_extra': _adjust_appmetadata_schema,
385
399
  }
386
400
 
387
401
  @pydantic.model_validator(mode='after')