wuenlp_tools.models.suspense.aggregation

  1from __future__ import annotations
  2
  3from typing import Type
  4
  5from loguru import logger
  6from wuenlp.impl.UIMANLPStructs import UIMADocument, UIMASystemScene
  7from wuenlp.impl.uima import UIMASpan
  8from wuenlp.impl.uima.extensions.danger import DangerMixin
  9
 10from wuenlp_tools.pipeline import (
 11    PipelineCapability,
 12    PipelineProcessor,
 13    PipelineStep,
 14    UnitFilter,
 15    get_units,
 16)
 17
 18# Feature keys used by the classification time-series builder
 19# (`experiments/classification/happy_end_2016/rich_timeseries.py`).
 20SUSPENSE_FEATURE_KEYS: tuple[str, ...] = (
 21    "bert_suspense_anysuspense",
 22    "bert_suspense_dangerous",
 23    "bert_suspense_fear",
 24)
 25
 26SUSPENSE_ANNOTATION_TYPES: dict[str, Type] = {
 27    "bert_suspense_anysuspense": DangerMixin.SystemAnySuspense,
 28    "bert_suspense_dangerous": DangerMixin.SystemDangerousSituation,
 29    "bert_suspense_fear": DangerMixin.SystemFearDescription,
 30}
 31
 32
 33def _spans_overlap(left_begin: int, left_end: int, right_begin: int, right_end: int) -> bool:
 34    return left_begin < right_end and right_begin < left_end
 35
 36
 37def _overlap_length(left_begin: int, left_end: int, right_begin: int, right_end: int) -> int:
 38    if not _spans_overlap(left_begin, left_end, right_begin, right_end):
 39        return 0
 40    return max(0, min(left_end, right_end) - max(left_begin, right_begin))
 41
 42
 43def _index_suspense_annotations(doc: UIMADocument) -> dict[str, list]:
 44    return {
 45        key: list(doc.get_annos_of_type(annotation_type))
 46        for key, annotation_type in SUSPENSE_ANNOTATION_TYPES.items()
 47    }
 48
 49
 50def scene_suspense_coverage(
 51        unit: UIMASpan,
 52        suspense_index: dict[str, list],
 53) -> dict[str, float]:
 54    """Covered fraction of ``unit`` per suspense type, clipped at 1."""
 55    unit_len = max(1, unit.end - unit.begin)
 56    values: dict[str, float] = {}
 57    for key in SUSPENSE_FEATURE_KEYS:
 58        covered = 0
 59        for annotation in suspense_index.get(key, []):
 60            covered += _overlap_length(unit.begin, unit.end, annotation.begin, annotation.end)
 61        values[key] = min(1.0, covered / unit_len)
 62    return values
 63
 64
 65class SuspenseAggregationProcessor(PipelineProcessor):
 66    """Aggregate BERT suspense spans to one coverage value per unit.
 67
 68    For each unit (default: scene), writes the fraction of the unit covered by
 69    ``SystemAnySuspense``, ``SystemDangerousSituation`` and
 70    ``SystemFearDescription`` spans, clipped at 1. This matches the thesis
 71    pipeline construction of one coverage value per suspense type per scene.
 72    """
 73
 74    def __call__(
 75            self,
 76            doc: UIMADocument,
 77            unit_type: Type[UIMASpan] | None,
 78            overwrite: bool = False,
 79            unit_filter: UnitFilter | None = None,
 80            **kwargs,
 81    ) -> UIMADocument:
 82        if unit_type is None:
 83            raise ValueError("SuspenseAggregationProcessor requires a non-None unit_type.")
 84
 85        units = get_units(doc, unit_type, unit_filter)
 86        if not units:
 87            logger.warning(f"No units found for {unit_type.__name__}; skipping suspense aggregation.")
 88            return doc
 89
 90        suspense_index = _index_suspense_annotations(doc)
 91        if not any(suspense_index.values()):
 92            logger.warning("No suspense span annotations found; writing zero coverage per unit.")
 93
 94        for unit in units:
 95            coverage = scene_suspense_coverage(unit, suspense_index)
 96            for key, value in coverage.items():
 97                if not overwrite and key in unit.additional_features:
 98                    continue
 99                unit.additional_features[key] = value
100
101        return doc
102
103
104def SuspenseAggregationStep(
105        unit_type: Type[UIMASpan] = UIMASystemScene,
106        name: str = "Suspense Aggregation",
107) -> PipelineStep[SuspenseAggregationProcessor]:
108    """Factory for the suspense coverage aggregator used in the thesis pipeline example."""
109    return PipelineStep(
110        name=name,
111        processor=SuspenseAggregationProcessor(),
112        unit_type=unit_type,
113        modified_types=[unit_type],
114        added_additional_features=list(SUSPENSE_FEATURE_KEYS),
115        added_mixins=DangerMixin,
116        requires=[PipelineCapability.SCENES],
117    )
118
119
120SuspenseAggregator = SuspenseAggregationStep
SUSPENSE_FEATURE_KEYS: tuple[str, ...] = ('bert_suspense_anysuspense', 'bert_suspense_dangerous', 'bert_suspense_fear')

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

SUSPENSE_ANNOTATION_TYPES: dict[str, typing.Type] = {'bert_suspense_anysuspense': <class 'wuenlp.impl.uima.extensions.danger.DangerMixin.SystemAnySuspense'>, 'bert_suspense_dangerous': <class 'wuenlp.impl.uima.extensions.danger.DangerMixin.SystemDangerousSituation'>, 'bert_suspense_fear': <class 'wuenlp.impl.uima.extensions.danger.DangerMixin.SystemFearDescription'>}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

def scene_suspense_coverage( unit: wuenlp.impl.uima.UIMANLPStructs.UIMASpan, suspense_index: dict[str, list]) -> dict[str, float]:
51def scene_suspense_coverage(
52        unit: UIMASpan,
53        suspense_index: dict[str, list],
54) -> dict[str, float]:
55    """Covered fraction of ``unit`` per suspense type, clipped at 1."""
56    unit_len = max(1, unit.end - unit.begin)
57    values: dict[str, float] = {}
58    for key in SUSPENSE_FEATURE_KEYS:
59        covered = 0
60        for annotation in suspense_index.get(key, []):
61            covered += _overlap_length(unit.begin, unit.end, annotation.begin, annotation.end)
62        values[key] = min(1.0, covered / unit_len)
63    return values

Covered fraction of unit per suspense type, clipped at 1.

 66class SuspenseAggregationProcessor(PipelineProcessor):
 67    """Aggregate BERT suspense spans to one coverage value per unit.
 68
 69    For each unit (default: scene), writes the fraction of the unit covered by
 70    ``SystemAnySuspense``, ``SystemDangerousSituation`` and
 71    ``SystemFearDescription`` spans, clipped at 1. This matches the thesis
 72    pipeline construction of one coverage value per suspense type per scene.
 73    """
 74
 75    def __call__(
 76            self,
 77            doc: UIMADocument,
 78            unit_type: Type[UIMASpan] | None,
 79            overwrite: bool = False,
 80            unit_filter: UnitFilter | None = None,
 81            **kwargs,
 82    ) -> UIMADocument:
 83        if unit_type is None:
 84            raise ValueError("SuspenseAggregationProcessor requires a non-None unit_type.")
 85
 86        units = get_units(doc, unit_type, unit_filter)
 87        if not units:
 88            logger.warning(f"No units found for {unit_type.__name__}; skipping suspense aggregation.")
 89            return doc
 90
 91        suspense_index = _index_suspense_annotations(doc)
 92        if not any(suspense_index.values()):
 93            logger.warning("No suspense span annotations found; writing zero coverage per unit.")
 94
 95        for unit in units:
 96            coverage = scene_suspense_coverage(unit, suspense_index)
 97            for key, value in coverage.items():
 98                if not overwrite and key in unit.additional_features:
 99                    continue
100                unit.additional_features[key] = value
101
102        return doc

Aggregate BERT suspense spans to one coverage value per unit.

For each unit (default: scene), writes the fraction of the unit covered by SystemAnySuspense, SystemDangerousSituation and SystemFearDescription spans, clipped at 1. This matches the thesis pipeline construction of one coverage value per suspense type per scene.

def SuspenseAggregationStep( unit_type: Type[wuenlp.impl.uima.UIMANLPStructs.UIMASpan] = <class 'wuenlp.impl.uima.UIMANLPStructs.UIMASystemScene'>, name: str = 'Suspense Aggregation') -> wuenlp_tools.pipeline.PipelineStep[SuspenseAggregationProcessor]:
105def SuspenseAggregationStep(
106        unit_type: Type[UIMASpan] = UIMASystemScene,
107        name: str = "Suspense Aggregation",
108) -> PipelineStep[SuspenseAggregationProcessor]:
109    """Factory for the suspense coverage aggregator used in the thesis pipeline example."""
110    return PipelineStep(
111        name=name,
112        processor=SuspenseAggregationProcessor(),
113        unit_type=unit_type,
114        modified_types=[unit_type],
115        added_additional_features=list(SUSPENSE_FEATURE_KEYS),
116        added_mixins=DangerMixin,
117        requires=[PipelineCapability.SCENES],
118    )

Factory for the suspense coverage aggregator used in the thesis pipeline example.

def SuspenseAggregator( unit_type: Type[wuenlp.impl.uima.UIMANLPStructs.UIMASpan] = <class 'wuenlp.impl.uima.UIMANLPStructs.UIMASystemScene'>, name: str = 'Suspense Aggregation') -> wuenlp_tools.pipeline.PipelineStep[SuspenseAggregationProcessor]:
105def SuspenseAggregationStep(
106        unit_type: Type[UIMASpan] = UIMASystemScene,
107        name: str = "Suspense Aggregation",
108) -> PipelineStep[SuspenseAggregationProcessor]:
109    """Factory for the suspense coverage aggregator used in the thesis pipeline example."""
110    return PipelineStep(
111        name=name,
112        processor=SuspenseAggregationProcessor(),
113        unit_type=unit_type,
114        modified_types=[unit_type],
115        added_additional_features=list(SUSPENSE_FEATURE_KEYS),
116        added_mixins=DangerMixin,
117        requires=[PipelineCapability.SCENES],
118    )

Factory for the suspense coverage aggregator used in the thesis pipeline example.