mmif-python 1.5.0__tar.gz → 1.5.2__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.
- mmif_python-1.5.2/.github/workflows/release-check.yml +11 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/CHANGELOG.md +30 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/CONTRIBUTING.md +9 -2
- {mmif_python-1.5.0/mmif_python.egg-info → mmif_python-1.5.2}/PKG-INFO +1 -1
- {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/docs.py +23 -8
- mmif_python-1.5.2/build-tools/prep_release.py +91 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/conf.py +0 -38
- mmif_python-1.5.2/documentation/target-versions.csv +53 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/annotation.py +50 -28
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/view.py +19 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/video_document_helper.py +22 -6
- {mmif_python-1.5.0 → mmif_python-1.5.2/mmif_python.egg-info}/PKG-INFO +1 -1
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/SOURCES.txt +5 -0
- mmif_python-1.5.2/mmif_python.egg-info/scm_file_list.json +95 -0
- mmif_python-1.5.2/mmif_python.egg-info/scm_version.json +8 -0
- mmif_python-1.5.2/tests/black-2997fps.webm +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_serialize.py +81 -2
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_utils.py +20 -0
- mmif_python-1.5.0/documentation/target-versions.csv +0 -47
- {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/codecov.yml +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/issue-assign.yml +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/issue-close.yml +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/issue-infra-project.yml +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/publish.yml +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/.gitignore +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/LICENSE +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/README.md +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/build.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/clean.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/publish.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/test.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/_mmif_example_builder/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/_templates/versions.html +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/cli.rst +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/consumer-tutorial.rst +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/index.rst +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/introduction.rst +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/plugins.rst +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/summarizer.rst +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/target-versions.rst +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/res/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/res/mmif.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/mmif.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/model.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/describe.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/rewind.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/source.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/summarize.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/config.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/graph.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/nodes.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/summary.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/utils.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/text_document_helper.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/workflow_helper.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/ver/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/vocabulary/base_types.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/vocabulary/document_types.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_docloc_http/__init__.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/entry_points.txt +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/requires.txt +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/pyproject.toml +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/setup.cfg +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/black-2997fps.mp4 +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/README.md +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/everything/raw.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/examples.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/README.md +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/image-box-ocr.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/image-box.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/image.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/slate-detect.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/slate-reco.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-demux.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/swt-1.0.mmif +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif_examples.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_specver.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_utils_cli.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_vocab.py +0 -0
- {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/testsrc-2997fps-ptsoffset.mp4 +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: "🔖 Release PR check"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
types: [opened, synchronize, reopened, edited]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release-check:
|
|
10
|
+
name: "🤙 Call SDK release-check workflow"
|
|
11
|
+
uses: clamsproject/.github/.github/workflows/sdk-release-check.yml@main
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
|
|
2
|
+
## releasing 1.5.1 (2026-06-19)
|
|
3
|
+
### Overview
|
|
4
|
+
This patch contains a bug fix in VDH that couldn't handle some mkv-contained video files (https://github.com/clamsproject/mmif-python/issues/390) , and some other minor documentation generation bugs.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## releasing 1.5.0 (2026-05-27)
|
|
8
|
+
### Overview
|
|
9
|
+
|
|
10
|
+
Improves image-extraction API in `video_document_helper` for TP source tracking, restores the source-composition prefix in workflow identifiers, and fixes the versioned-docs publishing pipeline.
|
|
11
|
+
|
|
12
|
+
### Additions
|
|
13
|
+
|
|
14
|
+
* `extract_images_by_count_with_sources` and `extract_images_by_mode_with_sources`: new public functions in `mmif.utils.video_document_helper` that return per-image source IDs (or sampled timepoints in ms when a fallback path is used), so downstream apps can anchor each extracted image to a specific point in the source media (https://github.com/clamsproject/mmif-python/pull/385, https://github.com/clamsproject/mmif-python/issues/384).
|
|
15
|
+
|
|
16
|
+
### Changes
|
|
17
|
+
|
|
18
|
+
* `mmif.utils.video_document_helper`: renamed
|
|
19
|
+
* `extract_timepoints_as_images` --> `extract_images_from_timepoints`,
|
|
20
|
+
* `extract_target_frames` --> `extract_images_by_count_with_sources` (alongside new bare `extract_images_by_count`), and
|
|
21
|
+
* `extract_frames_by_mode` --> `extract_images_by_mode`.
|
|
22
|
+
* The three pre-rename names remain importable as soft-deprecated aliases through 2.0.0 (https://github.com/clamsproject/mmif-python/pull/385).
|
|
23
|
+
* `mmif.utils.workflow_helper.generate_workflow_identifier`: now prefixes a `source_composition` segment (`TextDocument-1-VideoDocument-1/...`) describing the top-level document mix in the input MMIF (https://github.com/clamsproject/mmif-python/pull/386).
|
|
24
|
+
* `build-tools/docs.py`: versioned-docs build now works against pyproject-only tags, restoring docs publishing in the release workflow (https://github.com/clamsproject/mmif-python/pull/388, https://github.com/clamsproject/mmif-python/issues/387).
|
|
25
|
+
|
|
26
|
+
> [!NOTE]
|
|
27
|
+
> - `mmif describe` (and the underlying `mmif.utils.workflow_helper`) is still experimental and subject to change in future releases without notice. Backward compatibility is not guaranteed.
|
|
28
|
+
> - The new `source_composition` prefix in `generate_workflow_identifier()` changes the format of returned identifiers. Downstream tools parsing the identifier path (e.g. for storage-directory routing) may need to account for the extra leading segment.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
2
32
|
## releasing 1.4.0 (2026-04-22)
|
|
3
33
|
### Overview
|
|
4
34
|
* This is minor level release that 1) replacing simple string enums in `mmif.vocabulary` with fully-fledged pydantic classes in `clams-vocabulary` package 2) deprecating framenum-based image extraction functions in VDH (frame numbers are no longer considered as proper timestamps)
|
|
@@ -13,10 +13,17 @@ We follow a Gitflow-inspired branching model to maintain a stable `main` branch
|
|
|
13
13
|
- Once work is complete, open a PR targeting the `develop` branch.
|
|
14
14
|
- **Communication**: High-level discussion and planning should occur in the issue thread. The PR conversation is strictly for code review and implementation-specific feedback.
|
|
15
15
|
5. **Releases**:
|
|
16
|
-
- When `develop` is ready for a new release, open a PR from `develop` to `main` using the "release" PR template.
|
|
16
|
+
- When `develop` is ready for a new release, open a PR from `develop` to `main` using the "release" PR template. See the **Releases** section below for the required title format and preparation steps.
|
|
17
17
|
- After merging the release candidate into `main`, manually tag the commit with the version number. This tag triggers the automated CI/CD pipeline for publishing.
|
|
18
18
|
6. **Branch Protection**: Both `main` and `develop` are protected branches. Direct pushes are disabled; all changes must be introduced via Pull Requests.
|
|
19
19
|
|
|
20
|
+
## Releases
|
|
21
|
+
|
|
22
|
+
Release PRs (from `develop` to `main`) are gated by the `release-check` CI workflow. Before opening one:
|
|
23
|
+
|
|
24
|
+
1. Title the PR exactly `releasing X.Y.Z` (strict semver); `release-check` reads the version from the title.
|
|
25
|
+
2. Run `python build-tools/prep_release.py X.Y.Z` and commit its changes. See inside the prep script to see what's actually prepared.
|
|
26
|
+
|
|
20
27
|
## CLI Scripts
|
|
21
28
|
|
|
22
29
|
The `mmif` command-line interface supports subcommands (e.g., `mmif source`, `mmif describe`). These are implemented as Python modules in `mmif/utils/cli/`.
|
|
@@ -155,7 +162,7 @@ To build documentation for a specific historical version (e.g., `v1.0.0`):
|
|
|
155
162
|
python3 build-tools/docs.py --build-ver v1.0.0
|
|
156
163
|
```
|
|
157
164
|
|
|
158
|
-
This runs the build in a sandboxed temporary directory.
|
|
165
|
+
This runs the build in a sandboxed temporary directory. A single `--build-ver` places the built docs **flat** in the output directory (`docs-test/` by default). The batch `--build-all-since` mode instead nests each version under a `<version>/` subdirectory of the output directory, so multiple versions can coexist. The flat single-version layout is what the CI docs-publish workflow relies on — it invokes `docs.py --build-ver <ver> --output-dir _docs` and uploads `_docs/` as-is, without needing to know whether the project is versioned.
|
|
159
166
|
|
|
160
167
|
> [!NOTE]
|
|
161
168
|
> In CI, documentation is built and published automatically by the `publish.yml` workflow via the shared `sdk-docs.yml`. The CI calls `docs.py --build-ver <version> --output-dir _docs`. All CLAMS SDK repos use the same `docs.py` CLI interface (`--build-ver`, `--output-dir`).
|
|
@@ -188,7 +188,8 @@ def linkcode_resolve(domain, info):
|
|
|
188
188
|
return docs_build_dir
|
|
189
189
|
|
|
190
190
|
|
|
191
|
-
def build_docs_for_version(version: str, output_base_dir: Path, repo_path: Path = None
|
|
191
|
+
def build_docs_for_version(version: str, output_base_dir: Path, repo_path: Path = None,
|
|
192
|
+
versioned_subdir: bool = True):
|
|
192
193
|
"""
|
|
193
194
|
Builds docs for a specific version in a sandbox.
|
|
194
195
|
Uses local repo copy instead of cloning for efficiency.
|
|
@@ -197,6 +198,12 @@ def build_docs_for_version(version: str, output_base_dir: Path, repo_path: Path
|
|
|
197
198
|
version: Git tag/ref to build
|
|
198
199
|
output_base_dir: Directory to output built docs
|
|
199
200
|
repo_path: Path to local repo to copy from (defaults to cwd)
|
|
201
|
+
versioned_subdir: when True (batch/from-scratch rebuild), the built
|
|
202
|
+
docs are placed under ``output_base_dir/<version>/`` so multiple
|
|
203
|
+
versions can coexist. When False (single build, as used by CI),
|
|
204
|
+
the docs are placed flat into ``output_base_dir`` so the shared
|
|
205
|
+
docs-publish workflow can upload them as-is without knowing about
|
|
206
|
+
versioning.
|
|
200
207
|
"""
|
|
201
208
|
print(f"--- Running in Version Build Mode for version: {version} ---")
|
|
202
209
|
|
|
@@ -226,13 +233,19 @@ def build_docs_for_version(version: str, output_base_dir: Path, repo_path: Path
|
|
|
226
233
|
built_docs_path = build_versioned_docs(env, source_path, version)
|
|
227
234
|
|
|
228
235
|
print(f"\n--- Step 4: Copying built artifacts to output directory ---")
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
236
|
+
if versioned_subdir:
|
|
237
|
+
dest = output_base_dir / version
|
|
238
|
+
if dest.exists():
|
|
239
|
+
print(f"Removing existing directory: {dest}")
|
|
240
|
+
shutil.rmtree(dest)
|
|
241
|
+
shutil.copytree(built_docs_path, dest)
|
|
242
|
+
else:
|
|
243
|
+
# flat: place the built docs directly into output_base_dir
|
|
244
|
+
output_base_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
shutil.copytree(built_docs_path, output_base_dir, dirs_exist_ok=True)
|
|
246
|
+
dest = output_base_dir
|
|
234
247
|
|
|
235
|
-
print(f"\nDocumentation for {version} built successfully in: {
|
|
248
|
+
print(f"\nDocumentation for {version} built successfully in: {dest}")
|
|
236
249
|
|
|
237
250
|
|
|
238
251
|
def get_all_tags():
|
|
@@ -317,7 +330,9 @@ def main():
|
|
|
317
330
|
for version, error in failed:
|
|
318
331
|
print(f" {version}: {error}")
|
|
319
332
|
elif args.build_ver:
|
|
320
|
-
|
|
333
|
+
# single build (CI + local single-version): flat output so the
|
|
334
|
+
# shared docs-publish workflow uploads it version-agnostically
|
|
335
|
+
build_docs_for_version(args.build_ver, output_dir, versioned_subdir=False)
|
|
321
336
|
else:
|
|
322
337
|
build_docs_local(Path.cwd(), output_dir)
|
|
323
338
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prepare release artifacts for the mmif-python package.
|
|
3
|
+
|
|
4
|
+
Run while preparing a release PR, passing the version being released::
|
|
5
|
+
|
|
6
|
+
python build-tools/prep_release.py X.Y.Z
|
|
7
|
+
|
|
8
|
+
Currently this adds a row to ``documentation/target-versions.csv`` for
|
|
9
|
+
the release version, resolving the target MMIF spec version from the
|
|
10
|
+
installed package metadata (``mmif.__specver__``). This script is the
|
|
11
|
+
single place that writes the target-versions table; the docs build only
|
|
12
|
+
renders it, and CI verifies (on the release PR) that a row for the PR's
|
|
13
|
+
version exists.
|
|
14
|
+
|
|
15
|
+
Requires an editable install (``pip install -e .``) so that
|
|
16
|
+
``mmif.__specver__`` is importable.
|
|
17
|
+
"""
|
|
18
|
+
import argparse
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
PROJECT_ROOT = Path(__file__).parent.parent
|
|
24
|
+
CSV_PATH = PROJECT_ROOT / 'documentation' / 'target-versions.csv'
|
|
25
|
+
|
|
26
|
+
VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$')
|
|
27
|
+
TOP_ROW_RE = re.compile(r'`(\d+\.\d+\.\d+) <')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _row(version, specver):
|
|
31
|
+
return (
|
|
32
|
+
f'`{version} <https://pypi.org/project/mmif-python/{version}/>`__,'
|
|
33
|
+
f'`{specver} <https://mmif.clams.ai/{specver}/>`__'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _version_tuple(v):
|
|
38
|
+
return tuple(int(x) for x in v.split('.'))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _top_version(data_lines):
|
|
42
|
+
if not data_lines:
|
|
43
|
+
return None
|
|
44
|
+
m = TOP_ROW_RE.match(data_lines[0])
|
|
45
|
+
return m.group(1) if m else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
parser = argparse.ArgumentParser(
|
|
50
|
+
description="Prepare release artifacts for mmif-python."
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"version", metavar="X.Y.Z",
|
|
54
|
+
help="the release version being prepared",
|
|
55
|
+
)
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
version = args.version
|
|
58
|
+
|
|
59
|
+
if not VERSION_RE.match(version):
|
|
60
|
+
sys.exit(f"Error: '{version}' is not a valid X.Y.Z version.")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
import mmif
|
|
64
|
+
specver = mmif.__specver__
|
|
65
|
+
except (ImportError, AttributeError):
|
|
66
|
+
sys.exit("Error: cannot read mmif.__specver__. "
|
|
67
|
+
"Run `pip install -e .` first.")
|
|
68
|
+
|
|
69
|
+
lines = CSV_PATH.read_text().splitlines()
|
|
70
|
+
header, data = lines[0], lines[1:]
|
|
71
|
+
top = _top_version(data)
|
|
72
|
+
|
|
73
|
+
if top is not None and _version_tuple(version) < _version_tuple(top):
|
|
74
|
+
sys.exit(f"Error: {version} is older than the current top row "
|
|
75
|
+
f"{top}; refusing to insert an out-of-order version.")
|
|
76
|
+
|
|
77
|
+
new_row = _row(version, specver)
|
|
78
|
+
if top == version:
|
|
79
|
+
data[0] = new_row # update in place (e.g. spec changed)
|
|
80
|
+
action = "updated"
|
|
81
|
+
else:
|
|
82
|
+
data.insert(0, new_row) # prepend as the new latest
|
|
83
|
+
action = "added"
|
|
84
|
+
|
|
85
|
+
CSV_PATH.write_text('\n'.join([header] + data) + '\n')
|
|
86
|
+
print(f"target-versions.csv: {action} row "
|
|
87
|
+
f"{version} -> MMIF spec {specver}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|
|
@@ -6,7 +6,6 @@ import datetime
|
|
|
6
6
|
import inspect
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
-
import re
|
|
10
9
|
import subprocess
|
|
11
10
|
import sys
|
|
12
11
|
import textwrap
|
|
@@ -148,42 +147,6 @@ def linkcode_resolve(domain, info):
|
|
|
148
147
|
return None
|
|
149
148
|
|
|
150
149
|
|
|
151
|
-
def update_target_versions(app):
|
|
152
|
-
"""
|
|
153
|
-
Update documentation/target-versions.csv with the current version and spec version.
|
|
154
|
-
This replaces the old logic in setup.py.
|
|
155
|
-
"""
|
|
156
|
-
# Check if we have __specver__ (it might not be there if not installed via setup.py develop)
|
|
157
|
-
if not hasattr(mmif, '__specver__'):
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
current_ver = mmif.__version__
|
|
161
|
-
# Skip dev/dummy versions to avoid dirtying the git-tracked CSV
|
|
162
|
-
if 'dev' in current_ver or not re.match(r'^\d+\.\d+\.\d+$', current_ver):
|
|
163
|
-
return
|
|
164
|
-
spec_ver = mmif.__specver__
|
|
165
|
-
|
|
166
|
-
csv_path = proj_root_dir / 'documentation' / 'target-versions.csv'
|
|
167
|
-
if not csv_path.exists():
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
# Read existing content
|
|
171
|
-
with open(csv_path, 'r') as f:
|
|
172
|
-
lines = f.readlines()
|
|
173
|
-
|
|
174
|
-
# Check if current version is already in the file (first data line)
|
|
175
|
-
# lines[0] is header, lines[1] is latest version
|
|
176
|
-
if len(lines) > 1 and lines[1].startswith(f'{current_ver},'):
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
# Insert new version
|
|
180
|
-
logger.info(f"Updating target-versions.csv: {current_ver} -> {spec_ver}")
|
|
181
|
-
lines.insert(1, f'{current_ver},"{spec_ver}"\n')
|
|
182
|
-
|
|
183
|
-
with open(csv_path, 'w') as f:
|
|
184
|
-
f.writelines(lines)
|
|
185
|
-
|
|
186
|
-
|
|
187
150
|
def generate_cli_rst(app):
|
|
188
151
|
from mmif import prep_argparser_and_subcmds
|
|
189
152
|
|
|
@@ -292,7 +255,6 @@ def run_apidoc(app):
|
|
|
292
255
|
def setup(app):
|
|
293
256
|
try:
|
|
294
257
|
app.connect('builder-inited', run_apidoc)
|
|
295
|
-
app.connect('builder-inited', update_target_versions)
|
|
296
258
|
app.connect('builder-inited', generate_cli_rst)
|
|
297
259
|
app.connect('builder-inited', generate_whatsnew_rst)
|
|
298
260
|
except ImportError:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"``mmif-python`` version","Target MMIF Specification"
|
|
2
|
+
`1.5.2 <https://pypi.org/project/mmif-python/1.5.2/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
3
|
+
`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
4
|
+
`1.5.0 <https://pypi.org/project/mmif-python/1.5.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
5
|
+
`1.4.0 <https://pypi.org/project/mmif-python/1.4.0/>`__,`1.1.1 <https://mmif.clams.ai/1.1.1/>`__
|
|
6
|
+
`1.3.1 <https://pypi.org/project/mmif-python/1.3.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
|
|
7
|
+
`1.3.0 <https://pypi.org/project/mmif-python/1.3.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
|
|
8
|
+
`1.2.1 <https://pypi.org/project/mmif-python/1.2.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
|
|
9
|
+
`1.2.0 <https://pypi.org/project/mmif-python/1.2.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
|
|
10
|
+
`1.1.2 <https://pypi.org/project/mmif-python/1.1.2/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
|
|
11
|
+
`1.1.1 <https://pypi.org/project/mmif-python/1.1.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
|
|
12
|
+
`1.1.0 <https://pypi.org/project/mmif-python/1.1.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
|
|
13
|
+
`1.0.19 <https://pypi.org/project/mmif-python/1.0.19/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
|
|
14
|
+
`1.0.18 <https://pypi.org/project/mmif-python/1.0.18/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
|
|
15
|
+
`1.0.17 <https://pypi.org/project/mmif-python/1.0.17/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
|
|
16
|
+
`1.0.16 <https://pypi.org/project/mmif-python/1.0.16/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
|
|
17
|
+
`1.0.15 <https://pypi.org/project/mmif-python/1.0.15/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
|
|
18
|
+
`1.0.14 <https://pypi.org/project/mmif-python/1.0.14/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
|
|
19
|
+
`1.0.13 <https://pypi.org/project/mmif-python/1.0.13/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
|
|
20
|
+
`1.0.12 <https://pypi.org/project/mmif-python/1.0.12/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
|
|
21
|
+
`1.0.11 <https://pypi.org/project/mmif-python/1.0.11/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
|
|
22
|
+
`1.0.10 <https://pypi.org/project/mmif-python/1.0.10/>`__,`1.0.2 <https://mmif.clams.ai/1.0.2/>`__
|
|
23
|
+
`1.0.9 <https://pypi.org/project/mmif-python/1.0.9/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
|
|
24
|
+
`1.0.8 <https://pypi.org/project/mmif-python/1.0.8/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
|
|
25
|
+
`1.0.7 <https://pypi.org/project/mmif-python/1.0.7/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
|
|
26
|
+
`1.0.6 <https://pypi.org/project/mmif-python/1.0.6/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
|
|
27
|
+
`1.0.5 <https://pypi.org/project/mmif-python/1.0.5/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
|
|
28
|
+
`1.0.4 <https://pypi.org/project/mmif-python/1.0.4/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
|
|
29
|
+
`1.0.3 <https://pypi.org/project/mmif-python/1.0.3/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
|
|
30
|
+
`1.0.2 <https://pypi.org/project/mmif-python/1.0.2/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
|
|
31
|
+
`1.0.1 <https://pypi.org/project/mmif-python/1.0.1/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
|
|
32
|
+
`1.0.0 <https://pypi.org/project/mmif-python/1.0.0/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
|
|
33
|
+
`0.5.2 <https://pypi.org/project/mmif-python/0.5.2/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
|
|
34
|
+
`0.5.1 <https://pypi.org/project/mmif-python/0.5.1/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
|
|
35
|
+
`0.5.0 <https://pypi.org/project/mmif-python/0.5.0/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
|
|
36
|
+
`0.4.8 <https://pypi.org/project/mmif-python/0.4.8/>`__,`0.4.2 <https://mmif.clams.ai/0.4.2/>`__
|
|
37
|
+
`0.4.7 <https://pypi.org/project/mmif-python/0.4.7/>`__,`0.4.1 <https://mmif.clams.ai/0.4.1/>`__
|
|
38
|
+
`0.4.6 <https://pypi.org/project/mmif-python/0.4.6/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
|
|
39
|
+
`0.4.5 <https://pypi.org/project/mmif-python/0.4.5/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
|
|
40
|
+
`0.4.4 <https://pypi.org/project/mmif-python/0.4.4/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
|
|
41
|
+
`0.4.3 <https://pypi.org/project/mmif-python/0.4.3/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
|
|
42
|
+
`0.4.2 <https://pypi.org/project/mmif-python/0.4.2/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
|
|
43
|
+
`0.4.1 <https://pypi.org/project/mmif-python/0.4.1/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
|
|
44
|
+
`0.4.0 <https://pypi.org/project/mmif-python/0.4.0/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
|
|
45
|
+
`0.3.5 <https://pypi.org/project/mmif-python/0.3.5/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
|
|
46
|
+
`0.3.4 <https://pypi.org/project/mmif-python/0.3.4/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
|
|
47
|
+
`0.3.3 <https://pypi.org/project/mmif-python/0.3.3/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
|
|
48
|
+
`0.3.2 <https://pypi.org/project/mmif-python/0.3.2/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
|
|
49
|
+
`0.3.1 <https://pypi.org/project/mmif-python/0.3.1/>`__,`0.3.0 <https://mmif.clams.ai/0.3.0/>`__
|
|
50
|
+
`0.3.0 <https://pypi.org/project/mmif-python/0.3.0/>`__,`0.3.0 <https://mmif.clams.ai/0.3.0/>`__
|
|
51
|
+
`0.2.2 <https://pypi.org/project/mmif-python/0.2.2/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
|
|
52
|
+
`0.2.1 <https://pypi.org/project/mmif-python/0.2.1/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
|
|
53
|
+
`0.2.0 <https://pypi.org/project/mmif-python/0.2.0/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
|
|
@@ -414,22 +414,56 @@ class Document(Annotation):
|
|
|
414
414
|
else:
|
|
415
415
|
super().add_property(name, value)
|
|
416
416
|
|
|
417
|
-
def
|
|
417
|
+
def __getitem__(self, prop_name: str):
|
|
418
418
|
"""
|
|
419
|
-
|
|
419
|
+
Property access for ``Document`` objects, following the document
|
|
420
|
+
property hierarchy (highest to lowest priority):
|
|
420
421
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
422
|
+
#. Special fields (``id``, ``location``)
|
|
423
|
+
#. Pending properties (added via :meth:`add_property` after creation,
|
|
424
|
+
to be serialized as capital ``Annotation`` objects)
|
|
425
|
+
#. Original properties (in ``document.properties``)
|
|
426
|
+
#. Ephemeral properties (from existing ``Annotation`` objects or view
|
|
427
|
+
``contains`` metadata)
|
|
424
428
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
429
|
+
``Document`` overrides ``__getitem__`` (not only :meth:`get`) so that
|
|
430
|
+
the ``in`` operator, subscripting, and :meth:`get` all agree.
|
|
431
|
+
``__contains__`` is implemented on top of ``__getitem__``, so without
|
|
432
|
+
this override a property added via :meth:`add_property` (which lands in
|
|
433
|
+
``_props_pending``) is readable through :meth:`get` yet invisible to
|
|
434
|
+
``in`` until a serialize/deserialize round trip. See issue #390.
|
|
430
435
|
|
|
431
|
-
|
|
432
|
-
|
|
436
|
+
:param prop_name: the name of the property to retrieve
|
|
437
|
+
:raises KeyError: if the property is not found in any of the stores
|
|
438
|
+
"""
|
|
439
|
+
if prop_name == 'id':
|
|
440
|
+
# because all three dicts have `id` key as required field, we need
|
|
441
|
+
# this special case to return the correct value from the correct dict
|
|
442
|
+
return self.id
|
|
443
|
+
elif prop_name == 'location':
|
|
444
|
+
# location is internally stored as `location_`, so the regular
|
|
445
|
+
# lookup below never finds it. Treat an unset location as absent
|
|
446
|
+
# (raise) rather than returning None, so the inherited
|
|
447
|
+
# `__contains__` does not report `'location'` as always present.
|
|
448
|
+
if self.location is None:
|
|
449
|
+
raise KeyError(prop_name)
|
|
450
|
+
return self.location
|
|
451
|
+
elif prop_name in self._props_pending:
|
|
452
|
+
return self._props_pending[prop_name]
|
|
453
|
+
else:
|
|
454
|
+
# Delegates to Annotation.__getitem__(), which checks
|
|
455
|
+
# self.properties (_props_original) before _props_ephemeral.
|
|
456
|
+
# This gives annotation-level properties precedence over
|
|
457
|
+
# view-level contains defaults (MMIF spec >= 1.1.1).
|
|
458
|
+
return super().__getitem__(prop_name)
|
|
459
|
+
|
|
460
|
+
def get(self, prop_name, default=None):
|
|
461
|
+
"""
|
|
462
|
+
Safe property access with optional default value for Document objects.
|
|
463
|
+
|
|
464
|
+
Searches for a document property by name and returns its value, or
|
|
465
|
+
``default`` if not found, following the priority order documented on
|
|
466
|
+
:meth:`__getitem__`.
|
|
433
467
|
|
|
434
468
|
:param prop_name: The name of the property to retrieve
|
|
435
469
|
:param default: The value to return if the property is not found (default: None)
|
|
@@ -455,22 +489,10 @@ class Document(Annotation):
|
|
|
455
489
|
add_property : Add a new property to the document
|
|
456
490
|
mmif.serialize.mmif.Mmif.generate_capital_annotations : How pending properties are serialized
|
|
457
491
|
"""
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return
|
|
462
|
-
elif prop_name == 'location':
|
|
463
|
-
# because location is internally stored in self.location_,
|
|
464
|
-
# it doesn't work with regular __getitem__ method
|
|
465
|
-
return self.location
|
|
466
|
-
elif prop_name in self._props_pending:
|
|
467
|
-
return self._props_pending[prop_name]
|
|
468
|
-
else:
|
|
469
|
-
# Delegates to Annotation.__getitem__(), which checks
|
|
470
|
-
# self.properties (_props_original) before _props_ephemeral.
|
|
471
|
-
# This gives annotation-level properties precedence over
|
|
472
|
-
# view-level contains defaults (MMIF spec >= 1.1.1).
|
|
473
|
-
return super().get(prop_name, default)
|
|
492
|
+
try:
|
|
493
|
+
return self.__getitem__(prop_name)
|
|
494
|
+
except KeyError:
|
|
495
|
+
return default
|
|
474
496
|
|
|
475
497
|
get_property = get
|
|
476
498
|
|
|
@@ -82,6 +82,25 @@ class View(MmifObject):
|
|
|
82
82
|
self._required_attributes = ["id", "metadata", "annotations"]
|
|
83
83
|
super().__init__(view_obj)
|
|
84
84
|
self._fix_old_short_ids()
|
|
85
|
+
self._rebuild_id_counts()
|
|
86
|
+
|
|
87
|
+
def _rebuild_id_counts(self):
|
|
88
|
+
"""
|
|
89
|
+
Re-seed the per-view auto-ID counters from the annotations loaded during
|
|
90
|
+
deserialization.
|
|
91
|
+
|
|
92
|
+
``_id_counts`` is not serialized, so a view read back from JSON starts
|
|
93
|
+
with an empty counter. Without this, the next :meth:`new_annotation`
|
|
94
|
+
regenerates an ID (e.g. ``an_1``) that already exists in the
|
|
95
|
+
deserialized view and :meth:`AnnotationsList.append` raises ``KeyError``.
|
|
96
|
+
See issue #393.
|
|
97
|
+
"""
|
|
98
|
+
for annotation in self.annotations:
|
|
99
|
+
local_id = annotation.id.split(self.id_delimiter)[-1]
|
|
100
|
+
prefix, _, num = local_id.rpartition('_')
|
|
101
|
+
if prefix and num.isdigit():
|
|
102
|
+
self._id_counts[prefix] = max(
|
|
103
|
+
self._id_counts.get(prefix, 0), int(num))
|
|
85
104
|
|
|
86
105
|
def _fix_old_short_ids(self):
|
|
87
106
|
"""
|
|
@@ -96,20 +96,36 @@ def open_container(video_document: Document):
|
|
|
96
96
|
container = av.open(video_document.location_path(nonexist_ok=False))
|
|
97
97
|
stream = container.streams.video[0]
|
|
98
98
|
time_base = float(stream.time_base)
|
|
99
|
-
|
|
99
|
+
# Frame rate: prefer ffmpeg's guessed rate (av_guess_frame_rate), which
|
|
100
|
+
# falls back to the stream's base rate (r_frame_rate) when the average
|
|
101
|
+
# rate (avg_frame_rate) is unset. Matroska/webm commonly leave
|
|
102
|
+
# `average_rate` as None (which would crash float()) or 0, so it cannot be
|
|
103
|
+
# used directly. `rate` stays a Fraction for exact frame-count math; both
|
|
104
|
+
# None and a zero rate are falsy, so the chain yields the first usable one.
|
|
105
|
+
rate = stream.guessed_rate or stream.average_rate or stream.base_rate
|
|
106
|
+
fps = round(float(rate), 2) if rate else 0.0
|
|
107
|
+
# Resolve duration (in seconds) from the most reliable source available.
|
|
108
|
+
# `stream.duration` is exact for CFR H.264/MP4, but Matroska/webm leave it
|
|
109
|
+
# unset and carry the length only at the container level
|
|
110
|
+
# (`container.duration`, in `av.time_base` microseconds).
|
|
111
|
+
if stream.duration is not None:
|
|
112
|
+
duration_s = float(stream.duration) * time_base
|
|
113
|
+
elif container.duration is not None:
|
|
114
|
+
duration_s = container.duration / av.time_base
|
|
115
|
+
else:
|
|
116
|
+
duration_s = None
|
|
100
117
|
# `stream.frames` comes from the container header. Verified exact on
|
|
101
118
|
# CFR H.264/MP4 inputs even with non-zero start offset; for VFR or
|
|
102
119
|
# headerless streams it may be 0, in which case `duration * rate` is
|
|
103
120
|
# the best available (approximate) estimate.
|
|
104
121
|
if stream.frames > 0:
|
|
105
122
|
frame_count = stream.frames
|
|
106
|
-
elif
|
|
107
|
-
frame_count = int(round(float(
|
|
108
|
-
* float(stream.average_rate)))
|
|
123
|
+
elif duration_s is not None and rate:
|
|
124
|
+
frame_count = int(round(duration_s * float(rate)))
|
|
109
125
|
else:
|
|
110
126
|
frame_count = 0
|
|
111
|
-
if
|
|
112
|
-
duration_ms = int(round(
|
|
127
|
+
if duration_s is not None:
|
|
128
|
+
duration_ms = int(round(duration_s * 1000))
|
|
113
129
|
elif frame_count > 0 and fps > 0:
|
|
114
130
|
duration_ms = int(round(frame_count / fps * 1000))
|
|
115
131
|
else:
|
|
@@ -9,9 +9,11 @@ pyproject.toml
|
|
|
9
9
|
.github/workflows/issue-close.yml
|
|
10
10
|
.github/workflows/issue-infra-project.yml
|
|
11
11
|
.github/workflows/publish.yml
|
|
12
|
+
.github/workflows/release-check.yml
|
|
12
13
|
build-tools/build.py
|
|
13
14
|
build-tools/clean.py
|
|
14
15
|
build-tools/docs.py
|
|
16
|
+
build-tools/prep_release.py
|
|
15
17
|
build-tools/publish.py
|
|
16
18
|
build-tools/test.py
|
|
17
19
|
documentation/cli.rst
|
|
@@ -61,8 +63,11 @@ mmif_python.egg-info/SOURCES.txt
|
|
|
61
63
|
mmif_python.egg-info/dependency_links.txt
|
|
62
64
|
mmif_python.egg-info/entry_points.txt
|
|
63
65
|
mmif_python.egg-info/requires.txt
|
|
66
|
+
mmif_python.egg-info/scm_file_list.json
|
|
67
|
+
mmif_python.egg-info/scm_version.json
|
|
64
68
|
mmif_python.egg-info/top_level.txt
|
|
65
69
|
tests/black-2997fps.mp4
|
|
70
|
+
tests/black-2997fps.webm
|
|
66
71
|
tests/mmif_examples.py
|
|
67
72
|
tests/test_serialize.py
|
|
68
73
|
tests/test_specver.py
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
"README.md",
|
|
4
|
+
"LICENSE",
|
|
5
|
+
"pyproject.toml",
|
|
6
|
+
"CHANGELOG.md",
|
|
7
|
+
"CONTRIBUTING.md",
|
|
8
|
+
".gitignore",
|
|
9
|
+
"build-tools/publish.py",
|
|
10
|
+
"build-tools/prep_release.py",
|
|
11
|
+
"build-tools/build.py",
|
|
12
|
+
"build-tools/docs.py",
|
|
13
|
+
"build-tools/clean.py",
|
|
14
|
+
"build-tools/test.py",
|
|
15
|
+
"documentation/target-versions.csv",
|
|
16
|
+
"documentation/consumer-tutorial.rst",
|
|
17
|
+
"documentation/target-versions.rst",
|
|
18
|
+
"documentation/index.rst",
|
|
19
|
+
"documentation/introduction.rst",
|
|
20
|
+
"documentation/cli.rst",
|
|
21
|
+
"documentation/plugins.rst",
|
|
22
|
+
"documentation/summarizer.rst",
|
|
23
|
+
"documentation/conf.py",
|
|
24
|
+
"documentation/_mmif_example_builder/__init__.py",
|
|
25
|
+
"documentation/_templates/versions.html",
|
|
26
|
+
"mmif/__init__.py",
|
|
27
|
+
"mmif/vocabulary/__init__.py",
|
|
28
|
+
"mmif/vocabulary/base_types.py",
|
|
29
|
+
"mmif/vocabulary/document_types.py",
|
|
30
|
+
"mmif/vocabulary/annotation_types.py",
|
|
31
|
+
"mmif/utils/__init__.py",
|
|
32
|
+
"mmif/utils/text_document_helper.py",
|
|
33
|
+
"mmif/utils/timeunit_helper.py",
|
|
34
|
+
"mmif/utils/workflow_helper.py",
|
|
35
|
+
"mmif/utils/video_document_helper.py",
|
|
36
|
+
"mmif/utils/sequence_helper.py",
|
|
37
|
+
"mmif/utils/summarizer/__init__.py",
|
|
38
|
+
"mmif/utils/summarizer/config.py",
|
|
39
|
+
"mmif/utils/summarizer/utils.py",
|
|
40
|
+
"mmif/utils/summarizer/summary.py",
|
|
41
|
+
"mmif/utils/summarizer/graph.py",
|
|
42
|
+
"mmif/utils/summarizer/nodes.py",
|
|
43
|
+
"mmif/utils/cli/__init__.py",
|
|
44
|
+
"mmif/utils/cli/source.py",
|
|
45
|
+
"mmif/utils/cli/rewind.py",
|
|
46
|
+
"mmif/utils/cli/summarize.py",
|
|
47
|
+
"mmif/utils/cli/describe.py",
|
|
48
|
+
"mmif/serialize/model.py",
|
|
49
|
+
"mmif/serialize/__init__.py",
|
|
50
|
+
"mmif/serialize/view.py",
|
|
51
|
+
"mmif/serialize/annotation.py",
|
|
52
|
+
"mmif/serialize/mmif.py",
|
|
53
|
+
"mmif/res/__init__.py",
|
|
54
|
+
"mmif/res/mmif.json",
|
|
55
|
+
"mmif/ver/__init__.py",
|
|
56
|
+
"tests/mmif_examples.py",
|
|
57
|
+
"tests/test_specver.py",
|
|
58
|
+
"tests/test_utils.py",
|
|
59
|
+
"tests/test_utils_cli.py",
|
|
60
|
+
"tests/test_vocab.py",
|
|
61
|
+
"tests/testsrc-2997fps-ptsoffset.mp4",
|
|
62
|
+
"tests/test_serialize.py",
|
|
63
|
+
"tests/black-2997fps.mp4",
|
|
64
|
+
"tests/black-2997fps.webm",
|
|
65
|
+
"tests/mmif-examples/README.md",
|
|
66
|
+
"tests/mmif-examples/examples.json",
|
|
67
|
+
"tests/mmif-examples/1.0.5-old-shortid.json",
|
|
68
|
+
"tests/mmif-examples/swt-1.0.mmif",
|
|
69
|
+
"tests/mmif-examples/segmenter-kaldi-ner/raw.json",
|
|
70
|
+
"tests/mmif-examples/everything/raw.json",
|
|
71
|
+
"tests/mmif-examples/others/video-transcript.json",
|
|
72
|
+
"tests/mmif-examples/others/image-box-ocr-timex.json",
|
|
73
|
+
"tests/mmif-examples/others/README.md",
|
|
74
|
+
"tests/mmif-examples/others/video.json",
|
|
75
|
+
"tests/mmif-examples/others/video-transcript-demux-fa.json",
|
|
76
|
+
"tests/mmif-examples/others/image.json",
|
|
77
|
+
"tests/mmif-examples/others/video-transcript-sentencesplit.json",
|
|
78
|
+
"tests/mmif-examples/others/video-transcript-demux-fa.short.json",
|
|
79
|
+
"tests/mmif-examples/others/slate-detect.json",
|
|
80
|
+
"tests/mmif-examples/others/image-box.json",
|
|
81
|
+
"tests/mmif-examples/others/slate-reco.json",
|
|
82
|
+
"tests/mmif-examples/others/video-transcript-demux.json",
|
|
83
|
+
"tests/mmif-examples/others/image-box-ocr.json",
|
|
84
|
+
"tests/mmif-examples/others/video-demux.json",
|
|
85
|
+
"tests/mmif-examples/bars-tones-slates/raw.json",
|
|
86
|
+
"tests/mmif-examples/east-tesseract-typing/raw.json",
|
|
87
|
+
"mmif_docloc_http/__init__.py",
|
|
88
|
+
".github/workflows/issue-close.yml",
|
|
89
|
+
".github/workflows/codecov.yml",
|
|
90
|
+
".github/workflows/release-check.yml",
|
|
91
|
+
".github/workflows/issue-infra-project.yml",
|
|
92
|
+
".github/workflows/issue-assign.yml",
|
|
93
|
+
".github/workflows/publish.yml"
|
|
94
|
+
]
|
|
95
|
+
}
|
|
Binary file
|
|
@@ -1304,7 +1304,36 @@ class TestDocument(unittest.TestCase):
|
|
|
1304
1304
|
doc1_mmif_roundtrip = mmif_roundtrip['doc1']
|
|
1305
1305
|
self.assertEqual(0, len(doc1_mmif_roundtrip._props_pending))
|
|
1306
1306
|
self.assertEqual('me', doc1_mmif_roundtrip.get_property('author'))
|
|
1307
|
-
|
|
1307
|
+
|
|
1308
|
+
def test_document_pending_property_membership(self):
|
|
1309
|
+
# regression for issue #390: a property added via `add_property`
|
|
1310
|
+
# lands in `_props_pending` and must be visible through `get_property`,
|
|
1311
|
+
# subscripting, AND the `in` operator consistently -- without needing a
|
|
1312
|
+
# serialize/deserialize round trip first.
|
|
1313
|
+
doc = Document()
|
|
1314
|
+
doc.at_type = DocumentTypes.VideoDocument
|
|
1315
|
+
doc.id = 'doc1'
|
|
1316
|
+
doc.add_property('fps', 29.97)
|
|
1317
|
+
self.assertIn('fps', doc._props_pending)
|
|
1318
|
+
# all three access paths must agree
|
|
1319
|
+
self.assertEqual(29.97, doc.get_property('fps'))
|
|
1320
|
+
self.assertEqual(29.97, doc['fps'])
|
|
1321
|
+
self.assertIn('fps', doc)
|
|
1322
|
+
# special fields and absent keys are unaffected
|
|
1323
|
+
self.assertIn('id', doc)
|
|
1324
|
+
self.assertEqual('doc1', doc['id'])
|
|
1325
|
+
self.assertNotIn('nope', doc)
|
|
1326
|
+
with self.assertRaises(KeyError):
|
|
1327
|
+
_ = doc['nope']
|
|
1328
|
+
# an unset `location` must read as absent, not as always-present None
|
|
1329
|
+
self.assertNotIn('location', doc)
|
|
1330
|
+
with self.assertRaises(KeyError):
|
|
1331
|
+
_ = doc['location']
|
|
1332
|
+
self.assertIsNone(doc.get('location'))
|
|
1333
|
+
doc.location = 'file:///tmp/doc1.mp4'
|
|
1334
|
+
self.assertIn('location', doc)
|
|
1335
|
+
self.assertEqual('file:///tmp/doc1.mp4', doc['location'])
|
|
1336
|
+
|
|
1308
1337
|
def test_document_adding_duplicate_properties(self):
|
|
1309
1338
|
mmif = Mmif(self.data['everything']['string'])
|
|
1310
1339
|
doc1 = Document()
|
|
@@ -1422,7 +1451,57 @@ class TestDocument(unittest.TestCase):
|
|
|
1422
1451
|
'author'))
|
|
1423
1452
|
self.assertEqual('you', list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))[
|
|
1424
1453
|
0].get_property('author'))
|
|
1425
|
-
|
|
1454
|
+
|
|
1455
|
+
def test_view_id_counts_rebuilt_on_deserialize(self):
|
|
1456
|
+
# regression for issue #393: `_id_counts` is not serialized, so after
|
|
1457
|
+
# deserialization the next generated id must not collide with an existing
|
|
1458
|
+
# one already present in the view.
|
|
1459
|
+
mmif = Mmif(validate=False)
|
|
1460
|
+
doc = Document(); doc.at_type = DocumentTypes.VideoDocument
|
|
1461
|
+
doc.id = 'd1'; doc.location = 'file:///v.mp4'
|
|
1462
|
+
mmif.add_document(doc)
|
|
1463
|
+
v = mmif.new_view()
|
|
1464
|
+
v.metadata.app = tester_appname
|
|
1465
|
+
a1 = v.new_annotation(AnnotationTypes.Annotation, document='d1', frameCount=10)
|
|
1466
|
+
self.assertTrue(a1.id.endswith('an_1'))
|
|
1467
|
+
# round-trip: the reloaded view still contains `an_1` but starts with an
|
|
1468
|
+
# empty counter unless rebuilt
|
|
1469
|
+
v2 = Mmif(mmif.serialize())[v.id]
|
|
1470
|
+
a2 = v2.new_annotation(AnnotationTypes.Annotation, document='d1', duration=20)
|
|
1471
|
+
self.assertTrue(a2.id.endswith('an_2')) # must not regenerate an_1
|
|
1472
|
+
|
|
1473
|
+
def test_capital_annotation_no_collision_across_apps(self):
|
|
1474
|
+
# regression for issue #393: a downstream app re-deriving a document
|
|
1475
|
+
# property with a *different* value must not crash on serialize; the
|
|
1476
|
+
# distinct value is recorded as an additional capital annotation
|
|
1477
|
+
# (value-based de-dup preserves both assertions), not a colliding one.
|
|
1478
|
+
m = Mmif(validate=False)
|
|
1479
|
+
d = Document()
|
|
1480
|
+
d.at_type = DocumentTypes.VideoDocument
|
|
1481
|
+
d.id = 'd1'
|
|
1482
|
+
d.location = 'file:///v.mp4'
|
|
1483
|
+
m.add_document(d)
|
|
1484
|
+
v1 = m.new_view(); v1.metadata.app = 'http://app/producer'
|
|
1485
|
+
v1.new_annotation(AnnotationTypes.TimeFrame, label='bars')
|
|
1486
|
+
d.add_property('frameCount', 51248.0)
|
|
1487
|
+
d.add_property('duration', 1709977.0)
|
|
1488
|
+
|
|
1489
|
+
# downstream app loads it and re-derives a *different* duration value
|
|
1490
|
+
m2 = Mmif(m.serialize())
|
|
1491
|
+
d2 = m2['d1']
|
|
1492
|
+
v2 = m2.new_view(); v2.metadata.app = 'http://app/consumer'
|
|
1493
|
+
v2.new_annotation(AnnotationTypes.TimeFrame, label='caption')
|
|
1494
|
+
d2.add_property('frameCount', 51248) # same value -> de-duped away
|
|
1495
|
+
d2.add_property('duration', 1709975) # different value -> new annotation
|
|
1496
|
+
out = Mmif(m2.serialize()) # must not raise KeyError
|
|
1497
|
+
|
|
1498
|
+
caps = [a for view in out.views
|
|
1499
|
+
for a in view.get_annotations(AnnotationTypes.Annotation)]
|
|
1500
|
+
durations = {a.get_property('duration') for a in caps if 'duration' in a}
|
|
1501
|
+
# both distinct duration assertions coexist; neither is lost or overwritten
|
|
1502
|
+
self.assertEqual(2, len(caps))
|
|
1503
|
+
self.assertEqual({1709977.0, 1709975}, durations)
|
|
1504
|
+
|
|
1426
1505
|
def test_capital_annotation_generation_viewfinder(self):
|
|
1427
1506
|
mmif = Mmif(validate=False)
|
|
1428
1507
|
for i in range(1, 3):
|
|
@@ -183,6 +183,26 @@ class TestVideoDocumentHelper(unittest.TestCase):
|
|
|
183
183
|
finally:
|
|
184
184
|
c.close()
|
|
185
185
|
|
|
186
|
+
def test_open_container_matroska(self):
|
|
187
|
+
# regression for issue #390: Matroska/webm leave the per-stream
|
|
188
|
+
# `frames`/`duration` fields unset, so open_container must fall back to
|
|
189
|
+
# the container-level duration instead of caching frameCount/duration 0.
|
|
190
|
+
vd = Document({
|
|
191
|
+
"@type": "http://mmif.clams.ai/vocabulary/VideoDocument/v1",
|
|
192
|
+
"properties": {
|
|
193
|
+
"mime": "video",
|
|
194
|
+
"id": "o1",
|
|
195
|
+
"location": f"file://{pathlib.Path(__file__).parent}/black-2997fps.webm",
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
c = vdh.open_container(vd)
|
|
199
|
+
try:
|
|
200
|
+
self.assertAlmostEqual(29.97, vd.get_property('fps'), places=1)
|
|
201
|
+
self.assertGreater(vd.get_property('frameCount'), 0)
|
|
202
|
+
self.assertGreater(vd.get_property('duration'), 0)
|
|
203
|
+
finally:
|
|
204
|
+
c.close()
|
|
205
|
+
|
|
186
206
|
def test_sample_timepoints(self):
|
|
187
207
|
# half-open interval; step in ms
|
|
188
208
|
self.assertEqual([0, 100, 200, 300, 400],
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"``mmif-python`` version","Target MMIF Specification"
|
|
2
|
-
1.3.0,"1.1.0"
|
|
3
|
-
1.2.0,"1.1.0"
|
|
4
|
-
1.1.2,"1.1.0"
|
|
5
|
-
1.1.1,"1.1.0"
|
|
6
|
-
1.1.0,"1.1.0"
|
|
7
|
-
1.0.19,"1.0.5"
|
|
8
|
-
1.0.18,"1.0.5"
|
|
9
|
-
1.0.17,"1.0.5"
|
|
10
|
-
1.0.16,"1.0.4"
|
|
11
|
-
1.0.15,"1.0.4"
|
|
12
|
-
1.0.14,"1.0.4"
|
|
13
|
-
1.0.13,"1.0.4"
|
|
14
|
-
1.0.12,"1.0.4"
|
|
15
|
-
1.0.11,"1.0.4"
|
|
16
|
-
1.0.10,"1.0.2"
|
|
17
|
-
1.0.9,"1.1.0"
|
|
18
|
-
1.0.8,"1.0.0"
|
|
19
|
-
1.0.7,"1.0.0"
|
|
20
|
-
1.0.6,"1.0.0"
|
|
21
|
-
1.0.5,"1.0.0"
|
|
22
|
-
1.0.4,"1.0.0"
|
|
23
|
-
1.0.3,"1.0.0"
|
|
24
|
-
1.0.2,"1.0.0"
|
|
25
|
-
1.0.1,"1.0.0"
|
|
26
|
-
1.0.0,"0.5.0"
|
|
27
|
-
0.5.2,"0.5.0"
|
|
28
|
-
0.5.1,"0.5.0"
|
|
29
|
-
0.5.0,"0.5.0"
|
|
30
|
-
0.4.8,"0.4.2"
|
|
31
|
-
0.4.7,"0.4.1"
|
|
32
|
-
0.4.6,"0.4.0"
|
|
33
|
-
0.4.5,"0.4.0"
|
|
34
|
-
0.4.4,"0.4.0"
|
|
35
|
-
0.4.3,"0.4.0"
|
|
36
|
-
0.4.2,"0.4.0"
|
|
37
|
-
0.4.1,"0.4.0"
|
|
38
|
-
0.4.0,"0.4.0"
|
|
39
|
-
0.3.5,"0.3.1"
|
|
40
|
-
0.3.4,"0.3.1"
|
|
41
|
-
0.3.3,"0.3.1"
|
|
42
|
-
0.3.2,"0.3.1"
|
|
43
|
-
0.3.1,"0.3.0"
|
|
44
|
-
0.3.0,"0.3.0"
|
|
45
|
-
0.2.2,"0.2.1"
|
|
46
|
-
0.2.1,"0.2.1"
|
|
47
|
-
0.2.0,"0.2.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-demux-fa.json
RENAMED
|
File without changes
|
|
File without changes
|
{mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-demux.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|