praatio.data_classes.textgrid_tier

The abstract class used by all textgrid tiers

  1"""
  2The abstract class used by all textgrid tiers
  3"""
  4import re
  5import copy
  6import math
  7from typing import (
  8    List,
  9    Optional,
 10    Type,
 11    TypeVar,
 12    Union,
 13)
 14from abc import ABC, abstractmethod
 15
 16from typing_extensions import Literal
 17
 18
 19from praatio.utilities import constants
 20from praatio.utilities import errors
 21from praatio.utilities import utils
 22
 23T = TypeVar("T", bound="TextgridTier")
 24
 25
 26class TextgridTier(ABC):
 27    tierType: str
 28    entryType: Union[Type[constants.Point], Type[constants.Interval]]
 29
 30    def __init__(
 31        self,
 32        name: str,
 33        entries: List,
 34        minT: float,
 35        maxT: float,
 36        errorMode: Literal["silence", "warning", "error"] = "warning",
 37    ):
 38        "A container that stores and operates over interval and point tiers"
 39        utils.validateOption("errorMode", errorMode, constants.ErrorReportingMode)
 40
 41        """See PointTier or IntervalTier"""
 42        entries.sort()
 43
 44        self.name = name
 45        self._entries = entries
 46        self.minTimestamp = minT
 47        self.maxTimestamp = maxT
 48        self.errorReporter = utils.getErrorReporter(errorMode)
 49
 50    def __len__(self):
 51        return len(self._entries)
 52
 53    def __iter__(self):
 54        for entry in self.entries:
 55            yield entry
 56
 57    def __eq__(self, other):
 58        if not isinstance(self, type(other)):
 59            return False
 60
 61        isEqual = True
 62        isEqual &= self.name == other.name
 63        isEqual &= math.isclose(self.minTimestamp, other.minTimestamp)
 64        isEqual &= math.isclose(self.maxTimestamp, other.maxTimestamp)
 65        isEqual &= len(self.entries) == len(other.entries)
 66
 67        # TODO: Intervals and Points now use isclose, so we can simplify this
 68        #       logic (selfEntry == otherEntry); however, this will break
 69        #       things for klattgrids
 70        if isEqual:
 71            for selfEntry, otherEntry in zip(self.entries, other.entries):
 72                for selfSubEntry, otherSubEntry in zip(selfEntry, otherEntry):
 73                    try:
 74                        isEqual &= math.isclose(selfSubEntry, otherSubEntry)
 75                    except TypeError:
 76                        isEqual &= selfSubEntry == otherSubEntry
 77
 78        return isEqual
 79
 80    @property
 81    def entries(self):
 82        return tuple(self._entries)
 83
 84    @property
 85    @abstractmethod
 86    def timestamps(self) -> List[float]:
 87        pass
 88
 89    def appendTier(self, tier: "TextgridTier") -> "TextgridTier":
 90        """Append a tier to the end of this one.
 91
 92        This tier's maxtimestamp will be lengthened by the amount in the passed in tier.
 93        """
 94        if self.tierType != tier.tierType:
 95            raise errors.ArgumentError(
 96                f"Cannot append a tier of type {type(self)} to a tier of type {type(tier)}."
 97            )
 98
 99        maxTime = self.maxTimestamp + tier.maxTimestamp
100
101        # We're increasing the size of the tier, so of course its intervals
102        # may exceed the bounds of the tier's maxTimestamp, triggering
103        # errors/warnings--we can ignore those
104        appendTier = tier.editTimestamps(
105            self.maxTimestamp, constants.ErrorReportingMode.SILENCE
106        )
107
108        entries = self._entries + appendTier._entries
109        entries.sort()
110
111        return self.new(
112            self.name, entries, minTimestamp=self.minTimestamp, maxTimestamp=maxTime
113        )
114
115    def find(
116        self,
117        matchLabel: str,
118        substrMatchFlag: bool = False,
119        usingRE: bool = False,
120    ) -> List[int]:
121        """Returns the index of all intervals that match the given label
122
123        Args:
124            matchLabel: the label to search for
125            substrMatchFlag: if True, match any label containing matchLabel.
126                if False, label must be the same as matchLabel.
127            usingRE: if True, matchLabel is interpreted as a regular expression
128
129        Returns:
130            A list of indicies
131        """
132        returnList = []
133        if usingRE is True:
134            for i, entry in enumerate(self.entries):
135                matchList = re.findall(matchLabel, entry.label, re.I)
136                if matchList != []:
137                    returnList.append(i)
138        else:
139            for i, entry in enumerate(self.entries):
140                if not substrMatchFlag:
141                    if entry.label == matchLabel:
142                        returnList.append(i)
143                else:
144                    if matchLabel in entry.label:
145                        returnList.append(i)
146
147        return returnList
148
149    def new(
150        self: T,
151        name: Optional[str] = None,
152        entries: Optional[list] = None,
153        minTimestamp: Optional[float] = None,
154        maxTimestamp: Optional[float] = None,
155    ) -> T:
156        """Make a new tier derived from the current one"""
157        if name is None:
158            name = self.name
159        if entries is None:
160            entries = copy.deepcopy(self.entries)
161            entries = [
162                self.entryType(*entry)
163                if isinstance(entry, tuple) or isinstance(entry, list)
164                else entry
165                for entry in entries
166            ]
167        if minTimestamp is None:
168            minTimestamp = self.minTimestamp
169        if maxTimestamp is None:
170            maxTimestamp = self.maxTimestamp
171        return type(self)(name, entries, minTimestamp, maxTimestamp)
172
173    def sort(self) -> None:
174        """Sorts the entries in the list of entries"""
175        # A list containing tuples and lists will be sorted with tuples
176        # first and then lists.  To correctly sort, we need to make
177        # sure that all data structures inside the entry list are
178        # of the same data type.  The entry list is sorted whenever
179        # the entry list is modified, so this is probably the best
180        # place to enforce the data type
181        self._entries = [
182            entry if isinstance(entry, self.entryType) else self.entryType(*entry)
183            for entry in self.entries
184        ]
185        self._entries.sort()
186
187    def union(self, tier: "TextgridTier") -> "TextgridTier":
188        """The given tier is set unioned to this tier.
189
190        All entries in the given tier are added to the current tier.
191        Overlapping entries are merged.
192        """
193        retTier = self.new()
194
195        for entry in tier.entries:
196            retTier.insertEntry(
197                entry,
198                collisionMode=constants.IntervalCollision.MERGE,
199                collisionReportingMode=constants.ErrorReportingMode.SILENCE,
200            )
201
202        retTier.sort()
203
204        return retTier
205
206    @abstractmethod
207    def editTimestamps(
208        self,
209        offset: float,
210        reportingMode: Literal["silence", "warning", "error"] = "warning",
211    ) -> "TextgridTier":  # pragma: no cover
212        pass
213
214    @abstractmethod
215    def insertEntry(
216        self,
217        entry,
218        collisionMode: Literal["replace", "merge", "error"] = "error",
219        collisionReportingMode: Literal["silence", "warning"] = "warning",
220    ) -> None:  # pragma: no cover
221        pass
222
223    @abstractmethod
224    def dejitter(
225        self,
226        referenceTier: "TextgridTier",
227        maxDifference: float = 0.001,
228    ) -> "TextgridTier":  # pragma: no cover
229        pass
230
231    @abstractmethod
232    def eraseRegion(
233        self,
234        start: float,
235        end: float,
236        collisionMode: Literal["truncate", "categorical", "error"] = "error",
237        doShrink: bool = True,
238    ) -> "TextgridTier":  # pragma: no cover
239        pass
240
241    @abstractmethod
242    def crop(
243        self,
244        cropStart: float,
245        cropEnd: float,
246        mode: Literal["strict", "lax", "truncated"],
247        rebaseToZero: bool,
248    ) -> "TextgridTier":  # pragma: no cover
249        pass
250
251    @abstractmethod
252    def insertSpace(
253        self,
254        start: float,
255        duration: float,
256        collisionMode: Literal["stretch", "split", "no_change", "error"],
257    ) -> "TextgridTier":  # pragma: no cover
258        pass
259
260    @abstractmethod
261    def deleteEntry(self, entry) -> None:  # pragma: no cover
262        pass
263
264    @abstractmethod
265    def validate(self, reportingMode) -> bool:  # pragma: no cover
266        pass
class TextgridTier(abc.ABC):
 27class TextgridTier(ABC):
 28    tierType: str
 29    entryType: Union[Type[constants.Point], Type[constants.Interval]]
 30
 31    def __init__(
 32        self,
 33        name: str,
 34        entries: List,
 35        minT: float,
 36        maxT: float,
 37        errorMode: Literal["silence", "warning", "error"] = "warning",
 38    ):
 39        "A container that stores and operates over interval and point tiers"
 40        utils.validateOption("errorMode", errorMode, constants.ErrorReportingMode)
 41
 42        """See PointTier or IntervalTier"""
 43        entries.sort()
 44
 45        self.name = name
 46        self._entries = entries
 47        self.minTimestamp = minT
 48        self.maxTimestamp = maxT
 49        self.errorReporter = utils.getErrorReporter(errorMode)
 50
 51    def __len__(self):
 52        return len(self._entries)
 53
 54    def __iter__(self):
 55        for entry in self.entries:
 56            yield entry
 57
 58    def __eq__(self, other):
 59        if not isinstance(self, type(other)):
 60            return False
 61
 62        isEqual = True
 63        isEqual &= self.name == other.name
 64        isEqual &= math.isclose(self.minTimestamp, other.minTimestamp)
 65        isEqual &= math.isclose(self.maxTimestamp, other.maxTimestamp)
 66        isEqual &= len(self.entries) == len(other.entries)
 67
 68        # TODO: Intervals and Points now use isclose, so we can simplify this
 69        #       logic (selfEntry == otherEntry); however, this will break
 70        #       things for klattgrids
 71        if isEqual:
 72            for selfEntry, otherEntry in zip(self.entries, other.entries):
 73                for selfSubEntry, otherSubEntry in zip(selfEntry, otherEntry):
 74                    try:
 75                        isEqual &= math.isclose(selfSubEntry, otherSubEntry)
 76                    except TypeError:
 77                        isEqual &= selfSubEntry == otherSubEntry
 78
 79        return isEqual
 80
 81    @property
 82    def entries(self):
 83        return tuple(self._entries)
 84
 85    @property
 86    @abstractmethod
 87    def timestamps(self) -> List[float]:
 88        pass
 89
 90    def appendTier(self, tier: "TextgridTier") -> "TextgridTier":
 91        """Append a tier to the end of this one.
 92
 93        This tier's maxtimestamp will be lengthened by the amount in the passed in tier.
 94        """
 95        if self.tierType != tier.tierType:
 96            raise errors.ArgumentError(
 97                f"Cannot append a tier of type {type(self)} to a tier of type {type(tier)}."
 98            )
 99
100        maxTime = self.maxTimestamp + tier.maxTimestamp
101
102        # We're increasing the size of the tier, so of course its intervals
103        # may exceed the bounds of the tier's maxTimestamp, triggering
104        # errors/warnings--we can ignore those
105        appendTier = tier.editTimestamps(
106            self.maxTimestamp, constants.ErrorReportingMode.SILENCE
107        )
108
109        entries = self._entries + appendTier._entries
110        entries.sort()
111
112        return self.new(
113            self.name, entries, minTimestamp=self.minTimestamp, maxTimestamp=maxTime
114        )
115
116    def find(
117        self,
118        matchLabel: str,
119        substrMatchFlag: bool = False,
120        usingRE: bool = False,
121    ) -> List[int]:
122        """Returns the index of all intervals that match the given label
123
124        Args:
125            matchLabel: the label to search for
126            substrMatchFlag: if True, match any label containing matchLabel.
127                if False, label must be the same as matchLabel.
128            usingRE: if True, matchLabel is interpreted as a regular expression
129
130        Returns:
131            A list of indicies
132        """
133        returnList = []
134        if usingRE is True:
135            for i, entry in enumerate(self.entries):
136                matchList = re.findall(matchLabel, entry.label, re.I)
137                if matchList != []:
138                    returnList.append(i)
139        else:
140            for i, entry in enumerate(self.entries):
141                if not substrMatchFlag:
142                    if entry.label == matchLabel:
143                        returnList.append(i)
144                else:
145                    if matchLabel in entry.label:
146                        returnList.append(i)
147
148        return returnList
149
150    def new(
151        self: T,
152        name: Optional[str] = None,
153        entries: Optional[list] = None,
154        minTimestamp: Optional[float] = None,
155        maxTimestamp: Optional[float] = None,
156    ) -> T:
157        """Make a new tier derived from the current one"""
158        if name is None:
159            name = self.name
160        if entries is None:
161            entries = copy.deepcopy(self.entries)
162            entries = [
163                self.entryType(*entry)
164                if isinstance(entry, tuple) or isinstance(entry, list)
165                else entry
166                for entry in entries
167            ]
168        if minTimestamp is None:
169            minTimestamp = self.minTimestamp
170        if maxTimestamp is None:
171            maxTimestamp = self.maxTimestamp
172        return type(self)(name, entries, minTimestamp, maxTimestamp)
173
174    def sort(self) -> None:
175        """Sorts the entries in the list of entries"""
176        # A list containing tuples and lists will be sorted with tuples
177        # first and then lists.  To correctly sort, we need to make
178        # sure that all data structures inside the entry list are
179        # of the same data type.  The entry list is sorted whenever
180        # the entry list is modified, so this is probably the best
181        # place to enforce the data type
182        self._entries = [
183            entry if isinstance(entry, self.entryType) else self.entryType(*entry)
184            for entry in self.entries
185        ]
186        self._entries.sort()
187
188    def union(self, tier: "TextgridTier") -> "TextgridTier":
189        """The given tier is set unioned to this tier.
190
191        All entries in the given tier are added to the current tier.
192        Overlapping entries are merged.
193        """
194        retTier = self.new()
195
196        for entry in tier.entries:
197            retTier.insertEntry(
198                entry,
199                collisionMode=constants.IntervalCollision.MERGE,
200                collisionReportingMode=constants.ErrorReportingMode.SILENCE,
201            )
202
203        retTier.sort()
204
205        return retTier
206
207    @abstractmethod
208    def editTimestamps(
209        self,
210        offset: float,
211        reportingMode: Literal["silence", "warning", "error"] = "warning",
212    ) -> "TextgridTier":  # pragma: no cover
213        pass
214
215    @abstractmethod
216    def insertEntry(
217        self,
218        entry,
219        collisionMode: Literal["replace", "merge", "error"] = "error",
220        collisionReportingMode: Literal["silence", "warning"] = "warning",
221    ) -> None:  # pragma: no cover
222        pass
223
224    @abstractmethod
225    def dejitter(
226        self,
227        referenceTier: "TextgridTier",
228        maxDifference: float = 0.001,
229    ) -> "TextgridTier":  # pragma: no cover
230        pass
231
232    @abstractmethod
233    def eraseRegion(
234        self,
235        start: float,
236        end: float,
237        collisionMode: Literal["truncate", "categorical", "error"] = "error",
238        doShrink: bool = True,
239    ) -> "TextgridTier":  # pragma: no cover
240        pass
241
242    @abstractmethod
243    def crop(
244        self,
245        cropStart: float,
246        cropEnd: float,
247        mode: Literal["strict", "lax", "truncated"],
248        rebaseToZero: bool,
249    ) -> "TextgridTier":  # pragma: no cover
250        pass
251
252    @abstractmethod
253    def insertSpace(
254        self,
255        start: float,
256        duration: float,
257        collisionMode: Literal["stretch", "split", "no_change", "error"],
258    ) -> "TextgridTier":  # pragma: no cover
259        pass
260
261    @abstractmethod
262    def deleteEntry(self, entry) -> None:  # pragma: no cover
263        pass
264
265    @abstractmethod
266    def validate(self, reportingMode) -> bool:  # pragma: no cover
267        pass

Helper class that provides a standard way to create an ABC using inheritance.

TextgridTier( name: str, entries: List, minT: float, maxT: float, errorMode: Literal['silence', 'warning', 'error'] = 'warning')
31    def __init__(
32        self,
33        name: str,
34        entries: List,
35        minT: float,
36        maxT: float,
37        errorMode: Literal["silence", "warning", "error"] = "warning",
38    ):
39        "A container that stores and operates over interval and point tiers"
40        utils.validateOption("errorMode", errorMode, constants.ErrorReportingMode)
41
42        """See PointTier or IntervalTier"""
43        entries.sort()
44
45        self.name = name
46        self._entries = entries
47        self.minTimestamp = minT
48        self.maxTimestamp = maxT
49        self.errorReporter = utils.getErrorReporter(errorMode)

A container that stores and operates over interval and point tiers

tierType: str
name
minTimestamp
maxTimestamp
errorReporter
entries
timestamps: List[float]
def appendTier( self, tier: TextgridTier) -> TextgridTier:
 90    def appendTier(self, tier: "TextgridTier") -> "TextgridTier":
 91        """Append a tier to the end of this one.
 92
 93        This tier's maxtimestamp will be lengthened by the amount in the passed in tier.
 94        """
 95        if self.tierType != tier.tierType:
 96            raise errors.ArgumentError(
 97                f"Cannot append a tier of type {type(self)} to a tier of type {type(tier)}."
 98            )
 99
100        maxTime = self.maxTimestamp + tier.maxTimestamp
101
102        # We're increasing the size of the tier, so of course its intervals
103        # may exceed the bounds of the tier's maxTimestamp, triggering
104        # errors/warnings--we can ignore those
105        appendTier = tier.editTimestamps(
106            self.maxTimestamp, constants.ErrorReportingMode.SILENCE
107        )
108
109        entries = self._entries + appendTier._entries
110        entries.sort()
111
112        return self.new(
113            self.name, entries, minTimestamp=self.minTimestamp, maxTimestamp=maxTime
114        )

Append a tier to the end of this one.

This tier's maxtimestamp will be lengthened by the amount in the passed in tier.

def find( self, matchLabel: str, substrMatchFlag: bool = False, usingRE: bool = False) -> List[int]:
116    def find(
117        self,
118        matchLabel: str,
119        substrMatchFlag: bool = False,
120        usingRE: bool = False,
121    ) -> List[int]:
122        """Returns the index of all intervals that match the given label
123
124        Args:
125            matchLabel: the label to search for
126            substrMatchFlag: if True, match any label containing matchLabel.
127                if False, label must be the same as matchLabel.
128            usingRE: if True, matchLabel is interpreted as a regular expression
129
130        Returns:
131            A list of indicies
132        """
133        returnList = []
134        if usingRE is True:
135            for i, entry in enumerate(self.entries):
136                matchList = re.findall(matchLabel, entry.label, re.I)
137                if matchList != []:
138                    returnList.append(i)
139        else:
140            for i, entry in enumerate(self.entries):
141                if not substrMatchFlag:
142                    if entry.label == matchLabel:
143                        returnList.append(i)
144                else:
145                    if matchLabel in entry.label:
146                        returnList.append(i)
147
148        return returnList

Returns the index of all intervals that match the given label

Arguments:
  • matchLabel: the label to search for
  • substrMatchFlag: if True, match any label containing matchLabel. if False, label must be the same as matchLabel.
  • usingRE: if True, matchLabel is interpreted as a regular expression
Returns:

A list of indicies

def new( self: ~T, name: Optional[str] = None, entries: Optional[list] = None, minTimestamp: Optional[float] = None, maxTimestamp: Optional[float] = None) -> ~T:
150    def new(
151        self: T,
152        name: Optional[str] = None,
153        entries: Optional[list] = None,
154        minTimestamp: Optional[float] = None,
155        maxTimestamp: Optional[float] = None,
156    ) -> T:
157        """Make a new tier derived from the current one"""
158        if name is None:
159            name = self.name
160        if entries is None:
161            entries = copy.deepcopy(self.entries)
162            entries = [
163                self.entryType(*entry)
164                if isinstance(entry, tuple) or isinstance(entry, list)
165                else entry
166                for entry in entries
167            ]
168        if minTimestamp is None:
169            minTimestamp = self.minTimestamp
170        if maxTimestamp is None:
171            maxTimestamp = self.maxTimestamp
172        return type(self)(name, entries, minTimestamp, maxTimestamp)

Make a new tier derived from the current one

def sort(self) -> None:
174    def sort(self) -> None:
175        """Sorts the entries in the list of entries"""
176        # A list containing tuples and lists will be sorted with tuples
177        # first and then lists.  To correctly sort, we need to make
178        # sure that all data structures inside the entry list are
179        # of the same data type.  The entry list is sorted whenever
180        # the entry list is modified, so this is probably the best
181        # place to enforce the data type
182        self._entries = [
183            entry if isinstance(entry, self.entryType) else self.entryType(*entry)
184            for entry in self.entries
185        ]
186        self._entries.sort()

Sorts the entries in the list of entries

def union( self, tier: TextgridTier) -> TextgridTier:
188    def union(self, tier: "TextgridTier") -> "TextgridTier":
189        """The given tier is set unioned to this tier.
190
191        All entries in the given tier are added to the current tier.
192        Overlapping entries are merged.
193        """
194        retTier = self.new()
195
196        for entry in tier.entries:
197            retTier.insertEntry(
198                entry,
199                collisionMode=constants.IntervalCollision.MERGE,
200                collisionReportingMode=constants.ErrorReportingMode.SILENCE,
201            )
202
203        retTier.sort()
204
205        return retTier

The given tier is set unioned to this tier.

All entries in the given tier are added to the current tier. Overlapping entries are merged.

@abstractmethod
def editTimestamps( self, offset: float, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> TextgridTier:
207    @abstractmethod
208    def editTimestamps(
209        self,
210        offset: float,
211        reportingMode: Literal["silence", "warning", "error"] = "warning",
212    ) -> "TextgridTier":  # pragma: no cover
213        pass
@abstractmethod
def insertEntry( self, entry, collisionMode: Literal['replace', 'merge', 'error'] = 'error', collisionReportingMode: Literal['silence', 'warning'] = 'warning') -> None:
215    @abstractmethod
216    def insertEntry(
217        self,
218        entry,
219        collisionMode: Literal["replace", "merge", "error"] = "error",
220        collisionReportingMode: Literal["silence", "warning"] = "warning",
221    ) -> None:  # pragma: no cover
222        pass
@abstractmethod
def dejitter( self, referenceTier: TextgridTier, maxDifference: float = 0.001) -> TextgridTier:
224    @abstractmethod
225    def dejitter(
226        self,
227        referenceTier: "TextgridTier",
228        maxDifference: float = 0.001,
229    ) -> "TextgridTier":  # pragma: no cover
230        pass
@abstractmethod
def eraseRegion( self, start: float, end: float, collisionMode: Literal['truncate', 'categorical', 'error'] = 'error', doShrink: bool = True) -> TextgridTier:
232    @abstractmethod
233    def eraseRegion(
234        self,
235        start: float,
236        end: float,
237        collisionMode: Literal["truncate", "categorical", "error"] = "error",
238        doShrink: bool = True,
239    ) -> "TextgridTier":  # pragma: no cover
240        pass
@abstractmethod
def crop( self, cropStart: float, cropEnd: float, mode: Literal['strict', 'lax', 'truncated'], rebaseToZero: bool) -> TextgridTier:
242    @abstractmethod
243    def crop(
244        self,
245        cropStart: float,
246        cropEnd: float,
247        mode: Literal["strict", "lax", "truncated"],
248        rebaseToZero: bool,
249    ) -> "TextgridTier":  # pragma: no cover
250        pass
@abstractmethod
def insertSpace( self, start: float, duration: float, collisionMode: Literal['stretch', 'split', 'no_change', 'error']) -> TextgridTier:
252    @abstractmethod
253    def insertSpace(
254        self,
255        start: float,
256        duration: float,
257        collisionMode: Literal["stretch", "split", "no_change", "error"],
258    ) -> "TextgridTier":  # pragma: no cover
259        pass
@abstractmethod
def deleteEntry(self, entry) -> None:
261    @abstractmethod
262    def deleteEntry(self, entry) -> None:  # pragma: no cover
263        pass
@abstractmethod
def validate(self, reportingMode) -> bool:
265    @abstractmethod
266    def validate(self, reportingMode) -> bool:  # pragma: no cover
267        pass