praatio.data_classes.textgrid

Functions for reading/writing/manipulating Textgrid classes

This is the 'heart' of praatio.

  1"""
  2Functions for reading/writing/manipulating Textgrid classes
  3
  4This is the 'heart' of praatio.
  5"""
  6import io
  7import copy
  8from typing import Optional, Tuple, Sequence
  9from typing_extensions import Literal
 10from collections import OrderedDict
 11
 12
 13from praatio.utilities.constants import (
 14    TextgridFormats,
 15    MIN_INTERVAL_LENGTH,
 16    CropCollision,
 17)
 18
 19from praatio.data_classes import textgrid_tier
 20from praatio.data_classes import point_tier
 21from praatio.data_classes import interval_tier
 22from praatio.utilities import constants
 23from praatio.utilities import errors
 24from praatio.utilities import my_math
 25from praatio.utilities import textgrid_io
 26from praatio.utilities import utils
 27
 28
 29class Textgrid:
 30    """A container that stores and operates over interval and point tiers
 31
 32    Textgrids are used by the Praat software to group tiers.  Each tier
 33    contains different annotation information for an audio recording.
 34
 35    Attributes:
 36        tierNames(Tuple[str]): the list of tier names in the textgrid
 37        tiers(Tuple[TextgridTier]): the list of ordered tiers
 38        minTimestamp(float): the minimum allowable timestamp in the textgrid
 39        maxTimestamp(float): the maximum allowable timestamp in the textgrid
 40    """
 41
 42    def __init__(self, minTimestamp: float = None, maxTimestamp: float = None):
 43        """Constructor for Textgrids
 44
 45        Args:
 46            minTimestamp: the minimum allowable timestamp in the textgrid
 47            maxTimestamp: the maximum allowable timestamp in the textgrid
 48        """
 49
 50        self._tierDict: OrderedDict[str, textgrid_tier.TextgridTier] = OrderedDict()
 51
 52        # Timestamps are determined by the first tier added
 53        self.minTimestamp: float = minTimestamp  # type: ignore[assignment]
 54        self.maxTimestamp: float = maxTimestamp  # type: ignore[assignment]
 55
 56    def __len__(self):
 57        return len(self._tierDict)
 58
 59    def __iter__(self):
 60        for entry in self.tiers:
 61            yield entry
 62
 63    def __eq__(self, other):
 64        if not isinstance(other, Textgrid):
 65            return False
 66
 67        isEqual = True
 68        isEqual &= my_math.isclose(self.minTimestamp, other.minTimestamp)
 69        isEqual &= my_math.isclose(self.maxTimestamp, other.maxTimestamp)
 70
 71        isEqual &= self.tierNames == other.tierNames
 72        if isEqual:
 73            for tierName in self.tierNames:
 74                isEqual &= self.getTier(tierName) == other.getTier(tierName)
 75
 76        return isEqual
 77
 78    @property
 79    def tierNames(self) -> Tuple[str, ...]:
 80        return tuple(self._tierDict.keys())
 81
 82    @property
 83    def tiers(self) -> Tuple[textgrid_tier.TextgridTier, ...]:
 84        return tuple(self._tierDict.values())
 85
 86    def addTier(
 87        self,
 88        tier: textgrid_tier.TextgridTier,
 89        tierIndex: Optional[int] = None,
 90        reportingMode: Literal["silence", "warning", "error"] = "warning",
 91    ) -> None:
 92        """Add a tier to this textgrid.
 93
 94        Args:
 95            tier: The tier to add to the textgrid
 96            tierIndex: Insert the tier into the specified position;
 97                if None, the tier will appear after all existing tiers
 98            reportingMode: This flag determines the behavior if there is a size
 99                difference between the maxTimestamp in the tier and the current
100                textgrid.
101
102        Returns:
103            None
104
105        Raises:
106            TierNameExistsError: The textgrid already contains a tier with the same
107                name as the tier being added
108            TextgridStateAutoModified: The minimum or maximum timestamp was changed
109                when not permitted
110            IndexError: TierIndex is too large for the size of the existing tier list
111        """
112        utils.validateOption(
113            "reportingMode", reportingMode, constants.ErrorReportingMode
114        )
115        errorReporter = utils.getErrorReporter(reportingMode)
116
117        if tier.name in self.tierNames:
118            raise errors.TierNameExistsError("Tier name already in tier")
119
120        if tierIndex is None:
121            self._tierDict[tier.name] = tier
122        else:  # Need to recreate the tierDict with the new order
123            newOrderedTierNameList = list(self.tierNames)
124            newOrderedTierNameList.insert(tierIndex, tier.name)
125
126            newTierDict = OrderedDict()
127            self._tierDict[tier.name] = tier
128            for tmpName in newOrderedTierNameList:
129                newTierDict[tmpName] = self.getTier(tmpName)
130            self._tierDict = newTierDict
131
132        minV = tier.minTimestamp
133        if self.minTimestamp is not None and minV < self.minTimestamp:
134            errorReporter(
135                errors.TextgridStateAutoModified,
136                f"Minimum timestamp in Textgrid changed from ({self.minTimestamp}) to ({minV})",
137            )
138        if self.minTimestamp is None or minV < self.minTimestamp:
139            self.minTimestamp = minV
140
141        maxV = tier.maxTimestamp
142        if self.maxTimestamp is not None and maxV > self.maxTimestamp:
143            errorReporter(
144                errors.TextgridStateAutoModified,
145                f"Maximum timestamp in Textgrid changed from ({self.maxTimestamp}) to ({maxV})",
146            )
147        if self.maxTimestamp is None or maxV > self.maxTimestamp:
148            self.maxTimestamp = maxV
149
150    def appendTextgrid(self, tg: "Textgrid", onlyMatchingNames: bool) -> "Textgrid":
151        """Append one textgrid to the end of this one
152
153        Args:
154            tg: the textgrid to add to this one
155            onlyMatchingNames: if False, tiers that don't appear in both
156                textgrids will also appear
157
158        Returns:
159            the modified version of the current textgrid
160        """
161        minTime = self.minTimestamp
162        maxTime = self.maxTimestamp + tg.maxTimestamp
163        retTG = Textgrid(minTime, maxTime)
164
165        # Get all tier names.  Ordered first by this textgrid and
166        # then by the other textgrid.
167        combinedTierNames = list(self.tierNames)
168        for tierName in tg.tierNames:
169            if tierName not in combinedTierNames:
170                combinedTierNames.append(tierName)
171
172        # Determine the tier names that will be in the final textgrid
173        finalTierNames = []
174        if onlyMatchingNames is False:
175            finalTierNames = combinedTierNames
176        else:
177            for tierName in combinedTierNames:
178                if tierName in self.tierNames:
179                    if tierName in tg.tierNames:
180                        finalTierNames.append(tierName)
181
182        # Add tiers from this textgrid
183        for tierName in finalTierNames:
184            if tierName in self.tierNames:
185                tier = self.getTier(tierName)
186                retTG.addTier(tier)
187
188        # Add tiers from the given textgrid
189        for tierName in finalTierNames:
190            if tierName in tg.tierNames:
191                appendTier = tg.getTier(tierName)
192                appendTier = appendTier.new(minTimestamp=minTime, maxTimestamp=maxTime)
193
194                appendTier = appendTier.editTimestamps(self.maxTimestamp)
195
196                if tierName in retTG.tierNames:
197                    tier = retTG.getTier(tierName)
198                    newEntries = retTG.getTier(tierName).entries
199                    newEntries += appendTier.entries
200
201                    tier = tier.new(
202                        entries=newEntries,
203                        minTimestamp=minTime,
204                        maxTimestamp=maxTime,
205                    )
206                    retTG.replaceTier(tierName, tier)
207
208                else:
209                    tier = appendTier
210                    tier = tier.new(minTimestamp=minTime, maxTimestamp=maxTime)
211                    retTG.addTier(tier)
212
213        return retTG
214
215    def crop(
216        self,
217        cropStart: float,
218        cropEnd: float,
219        mode: Literal["strict", "lax", "truncated"],
220        rebaseToZero: bool,
221    ) -> "Textgrid":
222        """Creates a textgrid where all intervals fit within the crop region
223
224        Args:
225            cropStart: The start time of the crop interval
226            cropEnd: The stop time of the crop interval
227            mode: Determines the crop behavior
228                - 'strict', only intervals wholly contained by the crop
229                    interval will be kept
230                - 'lax', partially contained intervals will be kept
231                - 'truncated', partially contained intervals will be
232                    truncated to fit within the crop region.
233            rebaseToZero: if True, the cropped textgrid timestamps will be
234                subtracted by the cropStart; if False, timestamps will not
235                be changed
236
237        Returns:
238            the modified version of the current textgrid
239        """
240        utils.validateOption("mode", mode, CropCollision)
241
242        if cropStart >= cropEnd:
243            raise errors.ArgumentError(
244                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
245            )
246
247        if rebaseToZero is True:
248            minT = 0.0
249            maxT = cropEnd - cropStart
250        else:
251            minT = cropStart
252            maxT = cropEnd
253        newTG = Textgrid(minT, maxT)
254
255        for tierName in self.tierNames:
256            tier = self.getTier(tierName)
257            newTier = tier.crop(cropStart, cropEnd, mode, rebaseToZero)
258
259            reportingMode: Literal[
260                "silence", "warning", "error"
261            ] = constants.ErrorReportingMode.WARNING
262            if mode == constants.CropCollision.LAX:
263                # We expect that there will be changes to the size
264                # of the textgrid when the mode is LAX
265                reportingMode = constants.ErrorReportingMode.SILENCE
266
267            newTG.addTier(newTier, reportingMode=reportingMode)
268
269        return newTG
270
271    def eraseRegion(self, start: float, end: float, doShrink: bool) -> "Textgrid":
272        """Makes a region in a tier blank (removes all contained entries)
273
274        Intervals that span the region to erase will be truncated.
275
276        Args:
277            start:
278            end:
279            doShrink: if True, all entries appearing after the
280                erased interval will be shifted to fill the void (ie
281                the duration of the textgrid will be reduced by
282                *start* - *end*)
283
284        Returns:
285            the modified version of the current textgrid
286
287        Raises:
288            ArgumentError
289        """
290        if start >= end:
291            raise errors.ArgumentError(
292                f"EraseRegion error: start time ({start}) must occur before end time ({end})"
293            )
294
295        diff = end - start
296
297        maxTimestamp = self.maxTimestamp
298        if doShrink is True:
299            maxTimestamp -= diff
300
301        newTG = Textgrid(self.minTimestamp, self.maxTimestamp)
302        for tier in self.tiers:
303            shrunkTier = tier.eraseRegion(
304                start, end, constants.EraseCollision.TRUNCATE, doShrink
305            )
306            newTG.addTier(shrunkTier)
307
308        newTG.maxTimestamp = maxTimestamp
309
310        return newTG
311
312    def editTimestamps(
313        self,
314        offset: float,
315        reportingMode: Literal["silence", "warning", "error"] = "warning",
316    ) -> "Textgrid":
317        """Modifies all timestamps by a constant amount
318
319        Args:
320            offset: the amount to offset in seconds
321            reportingMode: one of "silence", "warning", or "error". This flag
322                determines the behavior if there is a size difference between the
323                maxTimestamp in the tier and the current textgrid.
324
325        Returns:
326            Textgrid: the modified version of the current textgrid
327        """
328        utils.validateOption(
329            "reportingMode", reportingMode, constants.ErrorReportingMode
330        )
331
332        tg = Textgrid(self.minTimestamp, self.maxTimestamp)
333        for tier in self.tiers:
334            if len(tier.entries) > 0:
335                tier = tier.editTimestamps(offset, reportingMode)
336
337            tg.addTier(tier, reportingMode=reportingMode)
338
339        return tg
340
341    def getTier(self, tierName: str) -> textgrid_tier.TextgridTier:
342        """Get the tier with the specified name"""
343        return self._tierDict[tierName]
344
345    def insertSpace(
346        self,
347        start: float,
348        duration: float,
349        collisionMode: Literal["stretch", "split", "no_change", "error"] = "error",
350    ) -> "Textgrid":
351        """Inserts a blank region into a textgrid
352
353        Every item that occurs after *start* will be pushed back by
354        *duration* seconds
355
356        Args:
357            start:
358            duration:
359            collisionMode: Determines behavior in the event that an
360                interval stradles the starting point.
361                One of ['stretch', 'split', 'no change', None]
362                - 'stretch' stretches the interval by /duration/ amount
363                - 'split' splits the interval into two--everything to the
364                    right of 'start' will be advanced by 'duration' seconds
365                - 'no change' leaves the interval as is with no change
366                - None or any other value throws an AssertionError
367
368        Returns:
369            Textgrid: the modified version of the current textgrid
370        """
371        utils.validateOption(
372            "collisionMode", collisionMode, constants.WhitespaceCollision
373        )
374
375        newTG = Textgrid(self.minTimestamp, self.maxTimestamp)
376        newTG.minTimestamp = self.minTimestamp
377        newTG.maxTimestamp = self.maxTimestamp + duration
378
379        for tier in self.tiers:
380            newTier = tier.insertSpace(start, duration, collisionMode)
381            newTG.addTier(newTier)
382
383        return newTG
384
385    def mergeTiers(
386        self, tierNames: Optional[Sequence[str]] = None, preserveOtherTiers: bool = True
387    ) -> "Textgrid":
388        """Combine tiers
389
390        Args:
391            tierList: A list of tier names to combine. If none, combine
392                all tiers.
393            preserveOtherTiers: If false, uncombined tiers are not
394                included in the output.
395
396        Returns:
397            Textgrid: the modified version of the current textgrid
398        """
399        if tierNames is None:
400            tierNames = self.tierNames
401
402        # Determine the tiers to merge
403        intervalTierNames = []
404        pointTierNames = []
405        for tierName in tierNames:
406            tier = self.getTier(tierName)
407            if isinstance(tier, interval_tier.IntervalTier):
408                intervalTierNames.append(tierName)
409            elif isinstance(tier, point_tier.PointTier):
410                pointTierNames.append(tierName)
411
412        # Merge the interval tiers
413        intervalTier = None
414        if len(intervalTierNames) > 0:
415            intervalTier = self.getTier(intervalTierNames.pop(0))
416            for tierName in intervalTierNames:
417                intervalTier = intervalTier.union(self.getTier(tierName))
418
419        # Merge the point tiers
420        pointTier = None
421        if len(pointTierNames) > 0:
422            pointTier = self.getTier(pointTierNames.pop(0))
423            for tierName in pointTierNames:
424                pointTier = pointTier.union(self.getTier(tierName))
425
426        # Create the final textgrid to output
427        tg = Textgrid(self.minTimestamp, self.maxTimestamp)
428
429        if preserveOtherTiers:
430            for tier in self.tiers:
431                if tier.name not in tierNames:
432                    tg.addTier(tier)
433
434        if intervalTier is not None:
435            tg.addTier(intervalTier)
436
437        if pointTier is not None:
438            tg.addTier(pointTier)
439
440        return tg
441
442    def new(self) -> "Textgrid":
443        """Returns a copy of this Textgrid"""
444        return copy.deepcopy(self)
445
446    def save(
447        self,
448        fn: str,
449        format: Literal["short_textgrid", "long_textgrid", "json", "textgrid_json"],
450        includeBlankSpaces: bool,
451        minTimestamp: Optional[float] = None,
452        maxTimestamp: Optional[float] = None,
453        minimumIntervalLength: float = MIN_INTERVAL_LENGTH,
454        reportingMode: Literal["silence", "warning", "error"] = "warning",
455    ) -> None:
456        """Save the current textgrid to a file
457
458        Args:
459            fn: the fullpath filename of the output
460            format: one of ['short_textgrid', 'long_textgrid', 'json', 'textgrid_json']
461                'short_textgrid' and 'long_textgrid' are both used by praat
462                'json' and 'textgrid_json' are two json variants. 'json' cannot represent
463                tiers with different min and max timestamps than the textgrid.
464            includeBlankSpaces: if True, blank sections in interval
465                tiers will be filled in with an empty interval
466                (with a label of ""). If you are unsure, True is recommended
467                as Praat needs blanks to render textgrids properly.
468            minTimestamp: the minTimestamp of the saved Textgrid;
469                if None, use whatever is defined in the Textgrid object.
470                If minTimestamp is larger than timestamps in your textgrid,
471                an exception will be thrown.
472            maxTimestamp: the maxTimestamp of the saved Textgrid;
473                if None, use whatever is defined in the Textgrid object.
474                If maxTimestamp is smaller than timestamps in your textgrid,
475                an exception will be thrown.
476            minimumIntervalLength: any labeled intervals smaller
477                than this will be removed, useful for removing ultrashort
478                or fragmented intervals; if None, don't remove any.
479                Removed intervals are merged (without their label) into
480                adjacent entries.
481            reportingMode: one of "silence", "warning", or "error". This flag
482                determines the behavior if there is a size difference between the
483                maxTimestamp in the tier and the current textgrid.
484
485        Returns:
486            a string representation of the textgrid
487        """
488
489        utils.validateOption("format", format, TextgridFormats)
490        utils.validateOption(
491            "reportingMode", reportingMode, constants.ErrorReportingMode
492        )
493
494        self.validate(reportingMode)
495
496        tgAsDict = _tgToDictionary(self)
497
498        textgridStr = textgrid_io.getTextgridAsStr(
499            tgAsDict,
500            format,
501            includeBlankSpaces,
502            minTimestamp,
503            maxTimestamp,
504            minimumIntervalLength,
505        )
506
507        with io.open(fn, "w", encoding="utf-8") as fd:
508            fd.write(textgridStr)
509
510    def renameTier(self, oldName: str, newName: str) -> None:
511        oldTier = self.getTier(oldName)
512        tierIndex = self.tierNames.index(oldName)
513        self.removeTier(oldName)
514        self.addTier(oldTier.new(newName, oldTier.entries), tierIndex)
515
516    def removeTier(self, name: str) -> textgrid_tier.TextgridTier:
517        return self._tierDict.pop(name)
518
519    def replaceTier(
520        self,
521        name: str,
522        newTier: textgrid_tier.TextgridTier,
523        reportingMode: Literal["silence", "warning", "error"] = "warning",
524    ) -> None:
525        tierIndex = self.tierNames.index(name)
526        self.removeTier(name)
527        self.addTier(newTier, tierIndex, reportingMode)
528
529    def validate(
530        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
531    ) -> bool:
532        """Validates this textgrid
533
534        Returns whether the textgrid is valid or not. If reportingMode is "warning"
535        or "error" this will also print on error or stop execution, respectively.
536
537        Args:
538            reportingMode: one of "silence", "warning", or "error". This flag
539                determines the behavior if there is a size difference between the
540                maxTimestamp in a tier and the current textgrid.
541
542        Returns:
543            True if this Textgrid is valid; False if not
544
545        Raises:
546            TierNameExistsError: Two tiers have the same name
547            TextgridStateError: A timestamp fall outside of the allowable range
548        """
549        utils.validateOption(
550            "reportingMode", reportingMode, constants.ErrorReportingMode
551        )
552        errorReporter = utils.getErrorReporter(reportingMode)
553
554        isValid = True
555        if len(self.tierNames) != len(set(self.tierNames)):
556            isValid = False
557            errorReporter(
558                errors.TierNameExistsError,
559                f"Tier names not unique: {self.tierNames}",
560            )
561
562        for tier in self.tiers:
563            if self.minTimestamp != tier.minTimestamp:
564                isValid = False
565                errorReporter(
566                    errors.TextgridStateError,
567                    f"Textgrid has a min timestamp of ({self.minTimestamp}) "
568                    f"but tier has ({tier.minTimestamp})",
569                )
570
571            if self.maxTimestamp != tier.maxTimestamp:
572                isValid = False
573                errorReporter(
574                    errors.TextgridStateError,
575                    f"Textgrid has a max timestamp of ({self.maxTimestamp}) "
576                    f"but tier has ({tier.maxTimestamp})",
577                )
578
579            isValid = isValid and tier.validate(reportingMode)
580
581        return isValid
582
583
584def _tgToDictionary(tg: Textgrid) -> dict:
585    tiers = []
586    for tier in tg.tiers:
587        tierAsDict = {
588            "class": tier.tierType,
589            "name": tier.name,
590            "xmin": tier.minTimestamp,
591            "xmax": tier.maxTimestamp,
592            "entries": tier.entries,
593        }
594        tiers.append(tierAsDict)
595
596    return {"xmin": tg.minTimestamp, "xmax": tg.maxTimestamp, "tiers": tiers}
class Textgrid:
 30class Textgrid:
 31    """A container that stores and operates over interval and point tiers
 32
 33    Textgrids are used by the Praat software to group tiers.  Each tier
 34    contains different annotation information for an audio recording.
 35
 36    Attributes:
 37        tierNames(Tuple[str]): the list of tier names in the textgrid
 38        tiers(Tuple[TextgridTier]): the list of ordered tiers
 39        minTimestamp(float): the minimum allowable timestamp in the textgrid
 40        maxTimestamp(float): the maximum allowable timestamp in the textgrid
 41    """
 42
 43    def __init__(self, minTimestamp: float = None, maxTimestamp: float = None):
 44        """Constructor for Textgrids
 45
 46        Args:
 47            minTimestamp: the minimum allowable timestamp in the textgrid
 48            maxTimestamp: the maximum allowable timestamp in the textgrid
 49        """
 50
 51        self._tierDict: OrderedDict[str, textgrid_tier.TextgridTier] = OrderedDict()
 52
 53        # Timestamps are determined by the first tier added
 54        self.minTimestamp: float = minTimestamp  # type: ignore[assignment]
 55        self.maxTimestamp: float = maxTimestamp  # type: ignore[assignment]
 56
 57    def __len__(self):
 58        return len(self._tierDict)
 59
 60    def __iter__(self):
 61        for entry in self.tiers:
 62            yield entry
 63
 64    def __eq__(self, other):
 65        if not isinstance(other, Textgrid):
 66            return False
 67
 68        isEqual = True
 69        isEqual &= my_math.isclose(self.minTimestamp, other.minTimestamp)
 70        isEqual &= my_math.isclose(self.maxTimestamp, other.maxTimestamp)
 71
 72        isEqual &= self.tierNames == other.tierNames
 73        if isEqual:
 74            for tierName in self.tierNames:
 75                isEqual &= self.getTier(tierName) == other.getTier(tierName)
 76
 77        return isEqual
 78
 79    @property
 80    def tierNames(self) -> Tuple[str, ...]:
 81        return tuple(self._tierDict.keys())
 82
 83    @property
 84    def tiers(self) -> Tuple[textgrid_tier.TextgridTier, ...]:
 85        return tuple(self._tierDict.values())
 86
 87    def addTier(
 88        self,
 89        tier: textgrid_tier.TextgridTier,
 90        tierIndex: Optional[int] = None,
 91        reportingMode: Literal["silence", "warning", "error"] = "warning",
 92    ) -> None:
 93        """Add a tier to this textgrid.
 94
 95        Args:
 96            tier: The tier to add to the textgrid
 97            tierIndex: Insert the tier into the specified position;
 98                if None, the tier will appear after all existing tiers
 99            reportingMode: This flag determines the behavior if there is a size
100                difference between the maxTimestamp in the tier and the current
101                textgrid.
102
103        Returns:
104            None
105
106        Raises:
107            TierNameExistsError: The textgrid already contains a tier with the same
108                name as the tier being added
109            TextgridStateAutoModified: The minimum or maximum timestamp was changed
110                when not permitted
111            IndexError: TierIndex is too large for the size of the existing tier list
112        """
113        utils.validateOption(
114            "reportingMode", reportingMode, constants.ErrorReportingMode
115        )
116        errorReporter = utils.getErrorReporter(reportingMode)
117
118        if tier.name in self.tierNames:
119            raise errors.TierNameExistsError("Tier name already in tier")
120
121        if tierIndex is None:
122            self._tierDict[tier.name] = tier
123        else:  # Need to recreate the tierDict with the new order
124            newOrderedTierNameList = list(self.tierNames)
125            newOrderedTierNameList.insert(tierIndex, tier.name)
126
127            newTierDict = OrderedDict()
128            self._tierDict[tier.name] = tier
129            for tmpName in newOrderedTierNameList:
130                newTierDict[tmpName] = self.getTier(tmpName)
131            self._tierDict = newTierDict
132
133        minV = tier.minTimestamp
134        if self.minTimestamp is not None and minV < self.minTimestamp:
135            errorReporter(
136                errors.TextgridStateAutoModified,
137                f"Minimum timestamp in Textgrid changed from ({self.minTimestamp}) to ({minV})",
138            )
139        if self.minTimestamp is None or minV < self.minTimestamp:
140            self.minTimestamp = minV
141
142        maxV = tier.maxTimestamp
143        if self.maxTimestamp is not None and maxV > self.maxTimestamp:
144            errorReporter(
145                errors.TextgridStateAutoModified,
146                f"Maximum timestamp in Textgrid changed from ({self.maxTimestamp}) to ({maxV})",
147            )
148        if self.maxTimestamp is None or maxV > self.maxTimestamp:
149            self.maxTimestamp = maxV
150
151    def appendTextgrid(self, tg: "Textgrid", onlyMatchingNames: bool) -> "Textgrid":
152        """Append one textgrid to the end of this one
153
154        Args:
155            tg: the textgrid to add to this one
156            onlyMatchingNames: if False, tiers that don't appear in both
157                textgrids will also appear
158
159        Returns:
160            the modified version of the current textgrid
161        """
162        minTime = self.minTimestamp
163        maxTime = self.maxTimestamp + tg.maxTimestamp
164        retTG = Textgrid(minTime, maxTime)
165
166        # Get all tier names.  Ordered first by this textgrid and
167        # then by the other textgrid.
168        combinedTierNames = list(self.tierNames)
169        for tierName in tg.tierNames:
170            if tierName not in combinedTierNames:
171                combinedTierNames.append(tierName)
172
173        # Determine the tier names that will be in the final textgrid
174        finalTierNames = []
175        if onlyMatchingNames is False:
176            finalTierNames = combinedTierNames
177        else:
178            for tierName in combinedTierNames:
179                if tierName in self.tierNames:
180                    if tierName in tg.tierNames:
181                        finalTierNames.append(tierName)
182
183        # Add tiers from this textgrid
184        for tierName in finalTierNames:
185            if tierName in self.tierNames:
186                tier = self.getTier(tierName)
187                retTG.addTier(tier)
188
189        # Add tiers from the given textgrid
190        for tierName in finalTierNames:
191            if tierName in tg.tierNames:
192                appendTier = tg.getTier(tierName)
193                appendTier = appendTier.new(minTimestamp=minTime, maxTimestamp=maxTime)
194
195                appendTier = appendTier.editTimestamps(self.maxTimestamp)
196
197                if tierName in retTG.tierNames:
198                    tier = retTG.getTier(tierName)
199                    newEntries = retTG.getTier(tierName).entries
200                    newEntries += appendTier.entries
201
202                    tier = tier.new(
203                        entries=newEntries,
204                        minTimestamp=minTime,
205                        maxTimestamp=maxTime,
206                    )
207                    retTG.replaceTier(tierName, tier)
208
209                else:
210                    tier = appendTier
211                    tier = tier.new(minTimestamp=minTime, maxTimestamp=maxTime)
212                    retTG.addTier(tier)
213
214        return retTG
215
216    def crop(
217        self,
218        cropStart: float,
219        cropEnd: float,
220        mode: Literal["strict", "lax", "truncated"],
221        rebaseToZero: bool,
222    ) -> "Textgrid":
223        """Creates a textgrid where all intervals fit within the crop region
224
225        Args:
226            cropStart: The start time of the crop interval
227            cropEnd: The stop time of the crop interval
228            mode: Determines the crop behavior
229                - 'strict', only intervals wholly contained by the crop
230                    interval will be kept
231                - 'lax', partially contained intervals will be kept
232                - 'truncated', partially contained intervals will be
233                    truncated to fit within the crop region.
234            rebaseToZero: if True, the cropped textgrid timestamps will be
235                subtracted by the cropStart; if False, timestamps will not
236                be changed
237
238        Returns:
239            the modified version of the current textgrid
240        """
241        utils.validateOption("mode", mode, CropCollision)
242
243        if cropStart >= cropEnd:
244            raise errors.ArgumentError(
245                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
246            )
247
248        if rebaseToZero is True:
249            minT = 0.0
250            maxT = cropEnd - cropStart
251        else:
252            minT = cropStart
253            maxT = cropEnd
254        newTG = Textgrid(minT, maxT)
255
256        for tierName in self.tierNames:
257            tier = self.getTier(tierName)
258            newTier = tier.crop(cropStart, cropEnd, mode, rebaseToZero)
259
260            reportingMode: Literal[
261                "silence", "warning", "error"
262            ] = constants.ErrorReportingMode.WARNING
263            if mode == constants.CropCollision.LAX:
264                # We expect that there will be changes to the size
265                # of the textgrid when the mode is LAX
266                reportingMode = constants.ErrorReportingMode.SILENCE
267
268            newTG.addTier(newTier, reportingMode=reportingMode)
269
270        return newTG
271
272    def eraseRegion(self, start: float, end: float, doShrink: bool) -> "Textgrid":
273        """Makes a region in a tier blank (removes all contained entries)
274
275        Intervals that span the region to erase will be truncated.
276
277        Args:
278            start:
279            end:
280            doShrink: if True, all entries appearing after the
281                erased interval will be shifted to fill the void (ie
282                the duration of the textgrid will be reduced by
283                *start* - *end*)
284
285        Returns:
286            the modified version of the current textgrid
287
288        Raises:
289            ArgumentError
290        """
291        if start >= end:
292            raise errors.ArgumentError(
293                f"EraseRegion error: start time ({start}) must occur before end time ({end})"
294            )
295
296        diff = end - start
297
298        maxTimestamp = self.maxTimestamp
299        if doShrink is True:
300            maxTimestamp -= diff
301
302        newTG = Textgrid(self.minTimestamp, self.maxTimestamp)
303        for tier in self.tiers:
304            shrunkTier = tier.eraseRegion(
305                start, end, constants.EraseCollision.TRUNCATE, doShrink
306            )
307            newTG.addTier(shrunkTier)
308
309        newTG.maxTimestamp = maxTimestamp
310
311        return newTG
312
313    def editTimestamps(
314        self,
315        offset: float,
316        reportingMode: Literal["silence", "warning", "error"] = "warning",
317    ) -> "Textgrid":
318        """Modifies all timestamps by a constant amount
319
320        Args:
321            offset: the amount to offset in seconds
322            reportingMode: one of "silence", "warning", or "error". This flag
323                determines the behavior if there is a size difference between the
324                maxTimestamp in the tier and the current textgrid.
325
326        Returns:
327            Textgrid: the modified version of the current textgrid
328        """
329        utils.validateOption(
330            "reportingMode", reportingMode, constants.ErrorReportingMode
331        )
332
333        tg = Textgrid(self.minTimestamp, self.maxTimestamp)
334        for tier in self.tiers:
335            if len(tier.entries) > 0:
336                tier = tier.editTimestamps(offset, reportingMode)
337
338            tg.addTier(tier, reportingMode=reportingMode)
339
340        return tg
341
342    def getTier(self, tierName: str) -> textgrid_tier.TextgridTier:
343        """Get the tier with the specified name"""
344        return self._tierDict[tierName]
345
346    def insertSpace(
347        self,
348        start: float,
349        duration: float,
350        collisionMode: Literal["stretch", "split", "no_change", "error"] = "error",
351    ) -> "Textgrid":
352        """Inserts a blank region into a textgrid
353
354        Every item that occurs after *start* will be pushed back by
355        *duration* seconds
356
357        Args:
358            start:
359            duration:
360            collisionMode: Determines behavior in the event that an
361                interval stradles the starting point.
362                One of ['stretch', 'split', 'no change', None]
363                - 'stretch' stretches the interval by /duration/ amount
364                - 'split' splits the interval into two--everything to the
365                    right of 'start' will be advanced by 'duration' seconds
366                - 'no change' leaves the interval as is with no change
367                - None or any other value throws an AssertionError
368
369        Returns:
370            Textgrid: the modified version of the current textgrid
371        """
372        utils.validateOption(
373            "collisionMode", collisionMode, constants.WhitespaceCollision
374        )
375
376        newTG = Textgrid(self.minTimestamp, self.maxTimestamp)
377        newTG.minTimestamp = self.minTimestamp
378        newTG.maxTimestamp = self.maxTimestamp + duration
379
380        for tier in self.tiers:
381            newTier = tier.insertSpace(start, duration, collisionMode)
382            newTG.addTier(newTier)
383
384        return newTG
385
386    def mergeTiers(
387        self, tierNames: Optional[Sequence[str]] = None, preserveOtherTiers: bool = True
388    ) -> "Textgrid":
389        """Combine tiers
390
391        Args:
392            tierList: A list of tier names to combine. If none, combine
393                all tiers.
394            preserveOtherTiers: If false, uncombined tiers are not
395                included in the output.
396
397        Returns:
398            Textgrid: the modified version of the current textgrid
399        """
400        if tierNames is None:
401            tierNames = self.tierNames
402
403        # Determine the tiers to merge
404        intervalTierNames = []
405        pointTierNames = []
406        for tierName in tierNames:
407            tier = self.getTier(tierName)
408            if isinstance(tier, interval_tier.IntervalTier):
409                intervalTierNames.append(tierName)
410            elif isinstance(tier, point_tier.PointTier):
411                pointTierNames.append(tierName)
412
413        # Merge the interval tiers
414        intervalTier = None
415        if len(intervalTierNames) > 0:
416            intervalTier = self.getTier(intervalTierNames.pop(0))
417            for tierName in intervalTierNames:
418                intervalTier = intervalTier.union(self.getTier(tierName))
419
420        # Merge the point tiers
421        pointTier = None
422        if len(pointTierNames) > 0:
423            pointTier = self.getTier(pointTierNames.pop(0))
424            for tierName in pointTierNames:
425                pointTier = pointTier.union(self.getTier(tierName))
426
427        # Create the final textgrid to output
428        tg = Textgrid(self.minTimestamp, self.maxTimestamp)
429
430        if preserveOtherTiers:
431            for tier in self.tiers:
432                if tier.name not in tierNames:
433                    tg.addTier(tier)
434
435        if intervalTier is not None:
436            tg.addTier(intervalTier)
437
438        if pointTier is not None:
439            tg.addTier(pointTier)
440
441        return tg
442
443    def new(self) -> "Textgrid":
444        """Returns a copy of this Textgrid"""
445        return copy.deepcopy(self)
446
447    def save(
448        self,
449        fn: str,
450        format: Literal["short_textgrid", "long_textgrid", "json", "textgrid_json"],
451        includeBlankSpaces: bool,
452        minTimestamp: Optional[float] = None,
453        maxTimestamp: Optional[float] = None,
454        minimumIntervalLength: float = MIN_INTERVAL_LENGTH,
455        reportingMode: Literal["silence", "warning", "error"] = "warning",
456    ) -> None:
457        """Save the current textgrid to a file
458
459        Args:
460            fn: the fullpath filename of the output
461            format: one of ['short_textgrid', 'long_textgrid', 'json', 'textgrid_json']
462                'short_textgrid' and 'long_textgrid' are both used by praat
463                'json' and 'textgrid_json' are two json variants. 'json' cannot represent
464                tiers with different min and max timestamps than the textgrid.
465            includeBlankSpaces: if True, blank sections in interval
466                tiers will be filled in with an empty interval
467                (with a label of ""). If you are unsure, True is recommended
468                as Praat needs blanks to render textgrids properly.
469            minTimestamp: the minTimestamp of the saved Textgrid;
470                if None, use whatever is defined in the Textgrid object.
471                If minTimestamp is larger than timestamps in your textgrid,
472                an exception will be thrown.
473            maxTimestamp: the maxTimestamp of the saved Textgrid;
474                if None, use whatever is defined in the Textgrid object.
475                If maxTimestamp is smaller than timestamps in your textgrid,
476                an exception will be thrown.
477            minimumIntervalLength: any labeled intervals smaller
478                than this will be removed, useful for removing ultrashort
479                or fragmented intervals; if None, don't remove any.
480                Removed intervals are merged (without their label) into
481                adjacent entries.
482            reportingMode: one of "silence", "warning", or "error". This flag
483                determines the behavior if there is a size difference between the
484                maxTimestamp in the tier and the current textgrid.
485
486        Returns:
487            a string representation of the textgrid
488        """
489
490        utils.validateOption("format", format, TextgridFormats)
491        utils.validateOption(
492            "reportingMode", reportingMode, constants.ErrorReportingMode
493        )
494
495        self.validate(reportingMode)
496
497        tgAsDict = _tgToDictionary(self)
498
499        textgridStr = textgrid_io.getTextgridAsStr(
500            tgAsDict,
501            format,
502            includeBlankSpaces,
503            minTimestamp,
504            maxTimestamp,
505            minimumIntervalLength,
506        )
507
508        with io.open(fn, "w", encoding="utf-8") as fd:
509            fd.write(textgridStr)
510
511    def renameTier(self, oldName: str, newName: str) -> None:
512        oldTier = self.getTier(oldName)
513        tierIndex = self.tierNames.index(oldName)
514        self.removeTier(oldName)
515        self.addTier(oldTier.new(newName, oldTier.entries), tierIndex)
516
517    def removeTier(self, name: str) -> textgrid_tier.TextgridTier:
518        return self._tierDict.pop(name)
519
520    def replaceTier(
521        self,
522        name: str,
523        newTier: textgrid_tier.TextgridTier,
524        reportingMode: Literal["silence", "warning", "error"] = "warning",
525    ) -> None:
526        tierIndex = self.tierNames.index(name)
527        self.removeTier(name)
528        self.addTier(newTier, tierIndex, reportingMode)
529
530    def validate(
531        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
532    ) -> bool:
533        """Validates this textgrid
534
535        Returns whether the textgrid is valid or not. If reportingMode is "warning"
536        or "error" this will also print on error or stop execution, respectively.
537
538        Args:
539            reportingMode: one of "silence", "warning", or "error". This flag
540                determines the behavior if there is a size difference between the
541                maxTimestamp in a tier and the current textgrid.
542
543        Returns:
544            True if this Textgrid is valid; False if not
545
546        Raises:
547            TierNameExistsError: Two tiers have the same name
548            TextgridStateError: A timestamp fall outside of the allowable range
549        """
550        utils.validateOption(
551            "reportingMode", reportingMode, constants.ErrorReportingMode
552        )
553        errorReporter = utils.getErrorReporter(reportingMode)
554
555        isValid = True
556        if len(self.tierNames) != len(set(self.tierNames)):
557            isValid = False
558            errorReporter(
559                errors.TierNameExistsError,
560                f"Tier names not unique: {self.tierNames}",
561            )
562
563        for tier in self.tiers:
564            if self.minTimestamp != tier.minTimestamp:
565                isValid = False
566                errorReporter(
567                    errors.TextgridStateError,
568                    f"Textgrid has a min timestamp of ({self.minTimestamp}) "
569                    f"but tier has ({tier.minTimestamp})",
570                )
571
572            if self.maxTimestamp != tier.maxTimestamp:
573                isValid = False
574                errorReporter(
575                    errors.TextgridStateError,
576                    f"Textgrid has a max timestamp of ({self.maxTimestamp}) "
577                    f"but tier has ({tier.maxTimestamp})",
578                )
579
580            isValid = isValid and tier.validate(reportingMode)
581
582        return isValid

A container that stores and operates over interval and point tiers

Textgrids are used by the Praat software to group tiers. Each tier contains different annotation information for an audio recording.

Attributes:
  • tierNames(Tuple[str]): the list of tier names in the textgrid
  • tiers(Tuple[TextgridTier]): the list of ordered tiers
  • minTimestamp(float): the minimum allowable timestamp in the textgrid
  • maxTimestamp(float): the maximum allowable timestamp in the textgrid
Textgrid(minTimestamp: float = None, maxTimestamp: float = None)
43    def __init__(self, minTimestamp: float = None, maxTimestamp: float = None):
44        """Constructor for Textgrids
45
46        Args:
47            minTimestamp: the minimum allowable timestamp in the textgrid
48            maxTimestamp: the maximum allowable timestamp in the textgrid
49        """
50
51        self._tierDict: OrderedDict[str, textgrid_tier.TextgridTier] = OrderedDict()
52
53        # Timestamps are determined by the first tier added
54        self.minTimestamp: float = minTimestamp  # type: ignore[assignment]
55        self.maxTimestamp: float = maxTimestamp  # type: ignore[assignment]

Constructor for Textgrids

Arguments:
  • minTimestamp: the minimum allowable timestamp in the textgrid
  • maxTimestamp: the maximum allowable timestamp in the textgrid
minTimestamp: float
maxTimestamp: float
tierNames: Tuple[str, ...]
def addTier( self, tier: praatio.data_classes.textgrid_tier.TextgridTier, tierIndex: Optional[int] = None, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> None:
 87    def addTier(
 88        self,
 89        tier: textgrid_tier.TextgridTier,
 90        tierIndex: Optional[int] = None,
 91        reportingMode: Literal["silence", "warning", "error"] = "warning",
 92    ) -> None:
 93        """Add a tier to this textgrid.
 94
 95        Args:
 96            tier: The tier to add to the textgrid
 97            tierIndex: Insert the tier into the specified position;
 98                if None, the tier will appear after all existing tiers
 99            reportingMode: This flag determines the behavior if there is a size
100                difference between the maxTimestamp in the tier and the current
101                textgrid.
102
103        Returns:
104            None
105
106        Raises:
107            TierNameExistsError: The textgrid already contains a tier with the same
108                name as the tier being added
109            TextgridStateAutoModified: The minimum or maximum timestamp was changed
110                when not permitted
111            IndexError: TierIndex is too large for the size of the existing tier list
112        """
113        utils.validateOption(
114            "reportingMode", reportingMode, constants.ErrorReportingMode
115        )
116        errorReporter = utils.getErrorReporter(reportingMode)
117
118        if tier.name in self.tierNames:
119            raise errors.TierNameExistsError("Tier name already in tier")
120
121        if tierIndex is None:
122            self._tierDict[tier.name] = tier
123        else:  # Need to recreate the tierDict with the new order
124            newOrderedTierNameList = list(self.tierNames)
125            newOrderedTierNameList.insert(tierIndex, tier.name)
126
127            newTierDict = OrderedDict()
128            self._tierDict[tier.name] = tier
129            for tmpName in newOrderedTierNameList:
130                newTierDict[tmpName] = self.getTier(tmpName)
131            self._tierDict = newTierDict
132
133        minV = tier.minTimestamp
134        if self.minTimestamp is not None and minV < self.minTimestamp:
135            errorReporter(
136                errors.TextgridStateAutoModified,
137                f"Minimum timestamp in Textgrid changed from ({self.minTimestamp}) to ({minV})",
138            )
139        if self.minTimestamp is None or minV < self.minTimestamp:
140            self.minTimestamp = minV
141
142        maxV = tier.maxTimestamp
143        if self.maxTimestamp is not None and maxV > self.maxTimestamp:
144            errorReporter(
145                errors.TextgridStateAutoModified,
146                f"Maximum timestamp in Textgrid changed from ({self.maxTimestamp}) to ({maxV})",
147            )
148        if self.maxTimestamp is None or maxV > self.maxTimestamp:
149            self.maxTimestamp = maxV

Add a tier to this textgrid.

Arguments:
  • tier: The tier to add to the textgrid
  • tierIndex: Insert the tier into the specified position; if None, the tier will appear after all existing tiers
  • reportingMode: This flag determines the behavior if there is a size difference between the maxTimestamp in the tier and the current textgrid.
Returns:

None

Raises:
  • TierNameExistsError: The textgrid already contains a tier with the same name as the tier being added
  • TextgridStateAutoModified: The minimum or maximum timestamp was changed when not permitted
  • IndexError: TierIndex is too large for the size of the existing tier list
def appendTextgrid( self, tg: Textgrid, onlyMatchingNames: bool) -> Textgrid:
151    def appendTextgrid(self, tg: "Textgrid", onlyMatchingNames: bool) -> "Textgrid":
152        """Append one textgrid to the end of this one
153
154        Args:
155            tg: the textgrid to add to this one
156            onlyMatchingNames: if False, tiers that don't appear in both
157                textgrids will also appear
158
159        Returns:
160            the modified version of the current textgrid
161        """
162        minTime = self.minTimestamp
163        maxTime = self.maxTimestamp + tg.maxTimestamp
164        retTG = Textgrid(minTime, maxTime)
165
166        # Get all tier names.  Ordered first by this textgrid and
167        # then by the other textgrid.
168        combinedTierNames = list(self.tierNames)
169        for tierName in tg.tierNames:
170            if tierName not in combinedTierNames:
171                combinedTierNames.append(tierName)
172
173        # Determine the tier names that will be in the final textgrid
174        finalTierNames = []
175        if onlyMatchingNames is False:
176            finalTierNames = combinedTierNames
177        else:
178            for tierName in combinedTierNames:
179                if tierName in self.tierNames:
180                    if tierName in tg.tierNames:
181                        finalTierNames.append(tierName)
182
183        # Add tiers from this textgrid
184        for tierName in finalTierNames:
185            if tierName in self.tierNames:
186                tier = self.getTier(tierName)
187                retTG.addTier(tier)
188
189        # Add tiers from the given textgrid
190        for tierName in finalTierNames:
191            if tierName in tg.tierNames:
192                appendTier = tg.getTier(tierName)
193                appendTier = appendTier.new(minTimestamp=minTime, maxTimestamp=maxTime)
194
195                appendTier = appendTier.editTimestamps(self.maxTimestamp)
196
197                if tierName in retTG.tierNames:
198                    tier = retTG.getTier(tierName)
199                    newEntries = retTG.getTier(tierName).entries
200                    newEntries += appendTier.entries
201
202                    tier = tier.new(
203                        entries=newEntries,
204                        minTimestamp=minTime,
205                        maxTimestamp=maxTime,
206                    )
207                    retTG.replaceTier(tierName, tier)
208
209                else:
210                    tier = appendTier
211                    tier = tier.new(minTimestamp=minTime, maxTimestamp=maxTime)
212                    retTG.addTier(tier)
213
214        return retTG

Append one textgrid to the end of this one

Arguments:
  • tg: the textgrid to add to this one
  • onlyMatchingNames: if False, tiers that don't appear in both textgrids will also appear
Returns:

the modified version of the current textgrid

def crop( self, cropStart: float, cropEnd: float, mode: Literal['strict', 'lax', 'truncated'], rebaseToZero: bool) -> Textgrid:
216    def crop(
217        self,
218        cropStart: float,
219        cropEnd: float,
220        mode: Literal["strict", "lax", "truncated"],
221        rebaseToZero: bool,
222    ) -> "Textgrid":
223        """Creates a textgrid where all intervals fit within the crop region
224
225        Args:
226            cropStart: The start time of the crop interval
227            cropEnd: The stop time of the crop interval
228            mode: Determines the crop behavior
229                - 'strict', only intervals wholly contained by the crop
230                    interval will be kept
231                - 'lax', partially contained intervals will be kept
232                - 'truncated', partially contained intervals will be
233                    truncated to fit within the crop region.
234            rebaseToZero: if True, the cropped textgrid timestamps will be
235                subtracted by the cropStart; if False, timestamps will not
236                be changed
237
238        Returns:
239            the modified version of the current textgrid
240        """
241        utils.validateOption("mode", mode, CropCollision)
242
243        if cropStart >= cropEnd:
244            raise errors.ArgumentError(
245                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
246            )
247
248        if rebaseToZero is True:
249            minT = 0.0
250            maxT = cropEnd - cropStart
251        else:
252            minT = cropStart
253            maxT = cropEnd
254        newTG = Textgrid(minT, maxT)
255
256        for tierName in self.tierNames:
257            tier = self.getTier(tierName)
258            newTier = tier.crop(cropStart, cropEnd, mode, rebaseToZero)
259
260            reportingMode: Literal[
261                "silence", "warning", "error"
262            ] = constants.ErrorReportingMode.WARNING
263            if mode == constants.CropCollision.LAX:
264                # We expect that there will be changes to the size
265                # of the textgrid when the mode is LAX
266                reportingMode = constants.ErrorReportingMode.SILENCE
267
268            newTG.addTier(newTier, reportingMode=reportingMode)
269
270        return newTG

Creates a textgrid where all intervals fit within the crop region

Arguments:
  • cropStart: The start time of the crop interval
  • cropEnd: The stop time of the crop interval
  • mode: Determines the crop behavior
    • 'strict', only intervals wholly contained by the crop interval will be kept
    • 'lax', partially contained intervals will be kept
    • 'truncated', partially contained intervals will be truncated to fit within the crop region.
  • rebaseToZero: if True, the cropped textgrid timestamps will be subtracted by the cropStart; if False, timestamps will not be changed
Returns:

the modified version of the current textgrid

def eraseRegion( self, start: float, end: float, doShrink: bool) -> Textgrid:
272    def eraseRegion(self, start: float, end: float, doShrink: bool) -> "Textgrid":
273        """Makes a region in a tier blank (removes all contained entries)
274
275        Intervals that span the region to erase will be truncated.
276
277        Args:
278            start:
279            end:
280            doShrink: if True, all entries appearing after the
281                erased interval will be shifted to fill the void (ie
282                the duration of the textgrid will be reduced by
283                *start* - *end*)
284
285        Returns:
286            the modified version of the current textgrid
287
288        Raises:
289            ArgumentError
290        """
291        if start >= end:
292            raise errors.ArgumentError(
293                f"EraseRegion error: start time ({start}) must occur before end time ({end})"
294            )
295
296        diff = end - start
297
298        maxTimestamp = self.maxTimestamp
299        if doShrink is True:
300            maxTimestamp -= diff
301
302        newTG = Textgrid(self.minTimestamp, self.maxTimestamp)
303        for tier in self.tiers:
304            shrunkTier = tier.eraseRegion(
305                start, end, constants.EraseCollision.TRUNCATE, doShrink
306            )
307            newTG.addTier(shrunkTier)
308
309        newTG.maxTimestamp = maxTimestamp
310
311        return newTG

Makes a region in a tier blank (removes all contained entries)

Intervals that span the region to erase will be truncated.

Arguments:
  • start:
  • end:
  • doShrink: if True, all entries appearing after the erased interval will be shifted to fill the void (ie the duration of the textgrid will be reduced by start - end)
Returns:

the modified version of the current textgrid

Raises:
  • ArgumentError
def editTimestamps( self, offset: float, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> Textgrid:
313    def editTimestamps(
314        self,
315        offset: float,
316        reportingMode: Literal["silence", "warning", "error"] = "warning",
317    ) -> "Textgrid":
318        """Modifies all timestamps by a constant amount
319
320        Args:
321            offset: the amount to offset in seconds
322            reportingMode: one of "silence", "warning", or "error". This flag
323                determines the behavior if there is a size difference between the
324                maxTimestamp in the tier and the current textgrid.
325
326        Returns:
327            Textgrid: the modified version of the current textgrid
328        """
329        utils.validateOption(
330            "reportingMode", reportingMode, constants.ErrorReportingMode
331        )
332
333        tg = Textgrid(self.minTimestamp, self.maxTimestamp)
334        for tier in self.tiers:
335            if len(tier.entries) > 0:
336                tier = tier.editTimestamps(offset, reportingMode)
337
338            tg.addTier(tier, reportingMode=reportingMode)
339
340        return tg

Modifies all timestamps by a constant amount

Arguments:
  • offset: the amount to offset in seconds
  • reportingMode: one of "silence", "warning", or "error". This flag determines the behavior if there is a size difference between the maxTimestamp in the tier and the current textgrid.
Returns:

Textgrid: the modified version of the current textgrid

def getTier(self, tierName: str) -> praatio.data_classes.textgrid_tier.TextgridTier:
342    def getTier(self, tierName: str) -> textgrid_tier.TextgridTier:
343        """Get the tier with the specified name"""
344        return self._tierDict[tierName]

Get the tier with the specified name

def insertSpace( self, start: float, duration: float, collisionMode: Literal['stretch', 'split', 'no_change', 'error'] = 'error') -> Textgrid:
346    def insertSpace(
347        self,
348        start: float,
349        duration: float,
350        collisionMode: Literal["stretch", "split", "no_change", "error"] = "error",
351    ) -> "Textgrid":
352        """Inserts a blank region into a textgrid
353
354        Every item that occurs after *start* will be pushed back by
355        *duration* seconds
356
357        Args:
358            start:
359            duration:
360            collisionMode: Determines behavior in the event that an
361                interval stradles the starting point.
362                One of ['stretch', 'split', 'no change', None]
363                - 'stretch' stretches the interval by /duration/ amount
364                - 'split' splits the interval into two--everything to the
365                    right of 'start' will be advanced by 'duration' seconds
366                - 'no change' leaves the interval as is with no change
367                - None or any other value throws an AssertionError
368
369        Returns:
370            Textgrid: the modified version of the current textgrid
371        """
372        utils.validateOption(
373            "collisionMode", collisionMode, constants.WhitespaceCollision
374        )
375
376        newTG = Textgrid(self.minTimestamp, self.maxTimestamp)
377        newTG.minTimestamp = self.minTimestamp
378        newTG.maxTimestamp = self.maxTimestamp + duration
379
380        for tier in self.tiers:
381            newTier = tier.insertSpace(start, duration, collisionMode)
382            newTG.addTier(newTier)
383
384        return newTG

Inserts a blank region into a textgrid

Every item that occurs after start will be pushed back by duration seconds

Arguments:
  • start:
  • duration:
  • collisionMode: Determines behavior in the event that an interval stradles the starting point. One of ['stretch', 'split', 'no change', None]
    • 'stretch' stretches the interval by /duration/ amount
    • 'split' splits the interval into two--everything to the right of 'start' will be advanced by 'duration' seconds
    • 'no change' leaves the interval as is with no change
    • None or any other value throws an AssertionError
Returns:

Textgrid: the modified version of the current textgrid

def mergeTiers( self, tierNames: Optional[Sequence[str]] = None, preserveOtherTiers: bool = True) -> Textgrid:
386    def mergeTiers(
387        self, tierNames: Optional[Sequence[str]] = None, preserveOtherTiers: bool = True
388    ) -> "Textgrid":
389        """Combine tiers
390
391        Args:
392            tierList: A list of tier names to combine. If none, combine
393                all tiers.
394            preserveOtherTiers: If false, uncombined tiers are not
395                included in the output.
396
397        Returns:
398            Textgrid: the modified version of the current textgrid
399        """
400        if tierNames is None:
401            tierNames = self.tierNames
402
403        # Determine the tiers to merge
404        intervalTierNames = []
405        pointTierNames = []
406        for tierName in tierNames:
407            tier = self.getTier(tierName)
408            if isinstance(tier, interval_tier.IntervalTier):
409                intervalTierNames.append(tierName)
410            elif isinstance(tier, point_tier.PointTier):
411                pointTierNames.append(tierName)
412
413        # Merge the interval tiers
414        intervalTier = None
415        if len(intervalTierNames) > 0:
416            intervalTier = self.getTier(intervalTierNames.pop(0))
417            for tierName in intervalTierNames:
418                intervalTier = intervalTier.union(self.getTier(tierName))
419
420        # Merge the point tiers
421        pointTier = None
422        if len(pointTierNames) > 0:
423            pointTier = self.getTier(pointTierNames.pop(0))
424            for tierName in pointTierNames:
425                pointTier = pointTier.union(self.getTier(tierName))
426
427        # Create the final textgrid to output
428        tg = Textgrid(self.minTimestamp, self.maxTimestamp)
429
430        if preserveOtherTiers:
431            for tier in self.tiers:
432                if tier.name not in tierNames:
433                    tg.addTier(tier)
434
435        if intervalTier is not None:
436            tg.addTier(intervalTier)
437
438        if pointTier is not None:
439            tg.addTier(pointTier)
440
441        return tg

Combine tiers

Arguments:
  • tierList: A list of tier names to combine. If none, combine all tiers.
  • preserveOtherTiers: If false, uncombined tiers are not included in the output.
Returns:

Textgrid: the modified version of the current textgrid

def new(self) -> Textgrid:
443    def new(self) -> "Textgrid":
444        """Returns a copy of this Textgrid"""
445        return copy.deepcopy(self)

Returns a copy of this Textgrid

def save( self, fn: str, format: Literal['short_textgrid', 'long_textgrid', 'json', 'textgrid_json'], includeBlankSpaces: bool, minTimestamp: Optional[float] = None, maxTimestamp: Optional[float] = None, minimumIntervalLength: float = 1e-08, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> None:
447    def save(
448        self,
449        fn: str,
450        format: Literal["short_textgrid", "long_textgrid", "json", "textgrid_json"],
451        includeBlankSpaces: bool,
452        minTimestamp: Optional[float] = None,
453        maxTimestamp: Optional[float] = None,
454        minimumIntervalLength: float = MIN_INTERVAL_LENGTH,
455        reportingMode: Literal["silence", "warning", "error"] = "warning",
456    ) -> None:
457        """Save the current textgrid to a file
458
459        Args:
460            fn: the fullpath filename of the output
461            format: one of ['short_textgrid', 'long_textgrid', 'json', 'textgrid_json']
462                'short_textgrid' and 'long_textgrid' are both used by praat
463                'json' and 'textgrid_json' are two json variants. 'json' cannot represent
464                tiers with different min and max timestamps than the textgrid.
465            includeBlankSpaces: if True, blank sections in interval
466                tiers will be filled in with an empty interval
467                (with a label of ""). If you are unsure, True is recommended
468                as Praat needs blanks to render textgrids properly.
469            minTimestamp: the minTimestamp of the saved Textgrid;
470                if None, use whatever is defined in the Textgrid object.
471                If minTimestamp is larger than timestamps in your textgrid,
472                an exception will be thrown.
473            maxTimestamp: the maxTimestamp of the saved Textgrid;
474                if None, use whatever is defined in the Textgrid object.
475                If maxTimestamp is smaller than timestamps in your textgrid,
476                an exception will be thrown.
477            minimumIntervalLength: any labeled intervals smaller
478                than this will be removed, useful for removing ultrashort
479                or fragmented intervals; if None, don't remove any.
480                Removed intervals are merged (without their label) into
481                adjacent entries.
482            reportingMode: one of "silence", "warning", or "error". This flag
483                determines the behavior if there is a size difference between the
484                maxTimestamp in the tier and the current textgrid.
485
486        Returns:
487            a string representation of the textgrid
488        """
489
490        utils.validateOption("format", format, TextgridFormats)
491        utils.validateOption(
492            "reportingMode", reportingMode, constants.ErrorReportingMode
493        )
494
495        self.validate(reportingMode)
496
497        tgAsDict = _tgToDictionary(self)
498
499        textgridStr = textgrid_io.getTextgridAsStr(
500            tgAsDict,
501            format,
502            includeBlankSpaces,
503            minTimestamp,
504            maxTimestamp,
505            minimumIntervalLength,
506        )
507
508        with io.open(fn, "w", encoding="utf-8") as fd:
509            fd.write(textgridStr)

Save the current textgrid to a file

Arguments:
  • fn: the fullpath filename of the output
  • format: one of ['short_textgrid', 'long_textgrid', 'json', 'textgrid_json'] 'short_textgrid' and 'long_textgrid' are both used by praat 'json' and 'textgrid_json' are two json variants. 'json' cannot represent tiers with different min and max timestamps than the textgrid.
  • includeBlankSpaces: if True, blank sections in interval tiers will be filled in with an empty interval (with a label of ""). If you are unsure, True is recommended as Praat needs blanks to render textgrids properly.
  • minTimestamp: the minTimestamp of the saved Textgrid; if None, use whatever is defined in the Textgrid object. If minTimestamp is larger than timestamps in your textgrid, an exception will be thrown.
  • maxTimestamp: the maxTimestamp of the saved Textgrid; if None, use whatever is defined in the Textgrid object. If maxTimestamp is smaller than timestamps in your textgrid, an exception will be thrown.
  • minimumIntervalLength: any labeled intervals smaller than this will be removed, useful for removing ultrashort or fragmented intervals; if None, don't remove any. Removed intervals are merged (without their label) into adjacent entries.
  • reportingMode: one of "silence", "warning", or "error". This flag determines the behavior if there is a size difference between the maxTimestamp in the tier and the current textgrid.
Returns:

a string representation of the textgrid

def renameTier(self, oldName: str, newName: str) -> None:
511    def renameTier(self, oldName: str, newName: str) -> None:
512        oldTier = self.getTier(oldName)
513        tierIndex = self.tierNames.index(oldName)
514        self.removeTier(oldName)
515        self.addTier(oldTier.new(newName, oldTier.entries), tierIndex)
def removeTier(self, name: str) -> praatio.data_classes.textgrid_tier.TextgridTier:
517    def removeTier(self, name: str) -> textgrid_tier.TextgridTier:
518        return self._tierDict.pop(name)
def replaceTier( self, name: str, newTier: praatio.data_classes.textgrid_tier.TextgridTier, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> None:
520    def replaceTier(
521        self,
522        name: str,
523        newTier: textgrid_tier.TextgridTier,
524        reportingMode: Literal["silence", "warning", "error"] = "warning",
525    ) -> None:
526        tierIndex = self.tierNames.index(name)
527        self.removeTier(name)
528        self.addTier(newTier, tierIndex, reportingMode)
def validate( self, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> bool:
530    def validate(
531        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
532    ) -> bool:
533        """Validates this textgrid
534
535        Returns whether the textgrid is valid or not. If reportingMode is "warning"
536        or "error" this will also print on error or stop execution, respectively.
537
538        Args:
539            reportingMode: one of "silence", "warning", or "error". This flag
540                determines the behavior if there is a size difference between the
541                maxTimestamp in a tier and the current textgrid.
542
543        Returns:
544            True if this Textgrid is valid; False if not
545
546        Raises:
547            TierNameExistsError: Two tiers have the same name
548            TextgridStateError: A timestamp fall outside of the allowable range
549        """
550        utils.validateOption(
551            "reportingMode", reportingMode, constants.ErrorReportingMode
552        )
553        errorReporter = utils.getErrorReporter(reportingMode)
554
555        isValid = True
556        if len(self.tierNames) != len(set(self.tierNames)):
557            isValid = False
558            errorReporter(
559                errors.TierNameExistsError,
560                f"Tier names not unique: {self.tierNames}",
561            )
562
563        for tier in self.tiers:
564            if self.minTimestamp != tier.minTimestamp:
565                isValid = False
566                errorReporter(
567                    errors.TextgridStateError,
568                    f"Textgrid has a min timestamp of ({self.minTimestamp}) "
569                    f"but tier has ({tier.minTimestamp})",
570                )
571
572            if self.maxTimestamp != tier.maxTimestamp:
573                isValid = False
574                errorReporter(
575                    errors.TextgridStateError,
576                    f"Textgrid has a max timestamp of ({self.maxTimestamp}) "
577                    f"but tier has ({tier.maxTimestamp})",
578                )
579
580            isValid = isValid and tier.validate(reportingMode)
581
582        return isValid

Validates this textgrid

Returns whether the textgrid is valid or not. If reportingMode is "warning" or "error" this will also print on error or stop execution, respectively.

Arguments:
  • reportingMode: one of "silence", "warning", or "error". This flag determines the behavior if there is a size difference between the maxTimestamp in a tier and the current textgrid.
Returns:

True if this Textgrid is valid; False if not

Raises:
  • TierNameExistsError: Two tiers have the same name
  • TextgridStateError: A timestamp fall outside of the allowable range