Repository navigation
Description
mmif-python handles document and annotation properties through three property stores on Document (original, pending, ephemeral) and a view-level default mechanism on Annotation. Together they serve two goals:
- automatic recording of document-level metadata, such as the technical metadata of videos, as capital
Annotationobjects (automatic document-levelAnnotationrecorder #226) - factoring shared property values out into a view's
containsmetadata and redistributing them on load (handling of annotation props incontainsmetadata #396)
This "three-dicts" approach is now largely broken. It has accumulated ad-hoc patches over the years (#290, #299, #393, #396, #401, #408, among others), and each fix added a rule that the next change had to respect. The latest round of bugs, found while preparing apps for mmif-python 1.6.0, shows that the problems are structural rather than local. It is time to revisit the fundamental design and fix the root causes where possible.
Root causes
- One store, many meanings.
_props_ephemeralholds values that differ in origin and lifetime: view-levelcontainsdefaults, values copied from capitalAnnotations onto documents, and computedstart/endcaches. Until canonicalize aliased property names instead of mirroring them #401 it also held property aliases. The precedence rules between them live in whichever code reads the store, and handling of annotation props incontainsmetadata #396 was one such collision. - Serialization changes the object it serializes.
serialize()generates capitalAnnotations into views, deletes factored values from annotation instances, and editscontains. As a result,annotation.propertiesmeans one thing before serialization and another after it. Since factor-out was generalized to all annotation types (improving shared-value property factoring #398) and turned on by default, every reader ofannotation.propertieshas silently become a reader of incomplete data.serialize(sanitize=True), which clams-python always uses, callsserialize()a second time insidesanitize()to produce the JSON it validates against the MMIF schema. That inner call generates capitalAnnotations and factors out values again, on an object the outer call has already changed, so the inconsistency shows up even within a single app run. - Pending document properties are never cleared, so the duplicate check in
generate_capital_annotations()is the only guard against recording the same property more than once. That check has been fixed repeatedly and still fails when values have been factored out. - The read-only promise does not hold. Input documents, and in principle every view an earlier app produced, should serialize unchanged however they are used in memory. Instead:
add_propertywritestext,mime, andlocationstraight into the document, so the serialized input document changes.- Values that fail the simple-value check also bypass the pending store and are written into the document.
- Factor-out runs over every view at serialization, so an earlier app's view is rewritten whenever its annotations share a value.
generate_capital_annotations()writes the current app's capitalAnnotationinto the latest view that annotates the document, which can be an earlier app's view (it shouldn't, but current bug allows this scenario).- The
add_propertydocstring states that existing properties cannot be overwritten.
Symptoms currently known (1.5.3 through 1.6.0):
- video document lookups fail on TimeFrames whose
documentis set at view level get_start/get_endfail on TimePoints whosetimePointwas factored out- document properties shared by two or more documents are lost on reload
- the same document properties are recorded twice on every app run
new_containsilently drops view-level defaults when the type is already declared- annotations added after a serialization inherit defaults they never had
Reader-side fixes for the first four are in progress as a stopgap for a 1.6.1 release. They do not address the causes above.
Plan
Investigate the best way to fix the root causes. This is the core mechanism behind much of mmif-python's "magic" groundwork: transparent property access, view-level defaults, and automatic metadata recording, all of which clams-python and every app rely on. It needs to be fixed as soon as possible.
The investigation should settle:
- whether the original goals still hold, and which parts of the current behavior apps depend on
- which constraints any fix must keep, such as MMIF output compatibility and the public property API
- how far the fix can go within the 1.x line, and what would have to wait for 2.0
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
keighrim commentedon Nov 1, 2026
#413 is open and removes the three-dicts approach, targeting 1.7.0. The output format stays at MMIF 1.2.0.
Root causes
Going through the four root causes in the issue body;
_props_ephemeralmixing different values: I stopped copyingcontainsdefaults onto annotations. An annotation now keeps a pointer to its parent view and looks up view-level values when they're read, soget_propertyandinwork for both levels, and.propertiesis only the raw stored data._props_ephemeralis still there for values copied from capitalAnnotations and for the cachedstart/end._props_originalis removed.serialize()changing the object: it now builds the output once, adds capital records and factored values to that output, and validates that same output, sosanitize=Truedoesn't serialize twice anymore. This also madeserialize(sanitize=True)faster on our largest test file (30,639 annotations), 5.35s --> 3.59s.All six symptoms listed in the issue body are fixed.
Write mode
mmif-python can't tell what "this run" is by looking at a
Mmif, so I added a write mode where the producer declares it;signatureis a dummy/orphanViewsigned by the producer. Namely, while the mode is on,containspruning, and empty-view removal touch the run's views onlyEmptyOutputWarningmoves here from clams-pythonOne exception to (1); old MMIF files that use
frameTypeare read aslabel, and that spelling is what gets written back. I think that's fine.Didn't add failure handling (
__exit()__) since exceptions just propagate, andrestifierandcli.pyalready have a good handle on errors.ClamsApp.annotate()will be the only caller, and_annotate()keeps working on the sameMmifobject, so app code doesn't need any change.Guardrail
Document.add_propertynow refuses to changelocation,text, ormimeonce they're set; a different value raisesAttributeError, and the same value does nothing. The spec already makes thedocumentslist read-only after initialization andmmif sourcefills it before the first app runs, so this check doesn't need to know about the write mode. It only coversadd_propertythough, anddoc.location = ...still goes through. The ownership-based version of this check is for 2.0 (there's aTODOat the check).Compatibility
Existing code should keep working without the write mode. As a check, I ran the clams-python 1.8.1 code with its pin lifted against #413, and it passes its tests with
DeprecationWarningignored. We won't release that combination. The next clams-python release based on this change in mmif-python (probably 1.8.2 as a patch) movesannotate()toset_producer().So I ran some tests with real apps on the upcoming mmif-py 1.7.0 and clams-py 1.8.2, with no change in app code;
app-swt-detectionon a 28-min video, GPU, Python 3.10: same TimePoints, TimeFrames, and capital record as the released SDK, and about the same run time (32s vs. 33s). The only other difference is the order oflabelset(because ofset's random seed).app-spacy-wrapperon two whisper transcripts: same annotations as 1.5.3 once view-level values are resolved, and the two SDK versions can read each other's output.Outside the write mode, the old behavior stays but marked as deprecated.
generate_capital_annotations()factor_out_shared_properties()sanitize()serialize()called with deprecated keywordsserialize(), in addition, raises aFutureWarningwhen it writes capital records,