praatio.data_classes.interval_tier
An IntervalTier is a tier containing an array of intervals -- data that spans a period of time
1""" 2An IntervalTier is a tier containing an array of intervals -- data that spans a period of time 3""" 4from typing import Callable, List, Optional, Tuple, Sequence 5 6from typing_extensions import Literal 7 8 9from praatio.utilities.constants import ( 10 Interval, 11 INTERVAL_TIER, 12 CropCollision, 13) 14 15from praatio.utilities import errors 16from praatio.utilities import utils 17from praatio.utilities import my_math 18from praatio.utilities import constants 19 20from praatio.data_classes import textgrid_tier 21 22 23def _homogenizeEntries(entries): 24 """ 25 Enforces consistency in intervals 26 27 - converts all entries to intervals 28 - removes whitespace in labels 29 - sorts values by time 30 """ 31 processedEntries = [ 32 Interval(float(start), float(end), label.strip()) 33 for start, end, label in entries 34 ] 35 processedEntries.sort() 36 return processedEntries 37 38 39def _calculateMinAndMaxTime(entries: Sequence[Interval], minT=None, maxT=None): 40 minTimeList = [interval.start for interval in entries] 41 maxTimeList = [interval.end for interval in entries] 42 43 if minT is not None: 44 minTimeList.append(float(minT)) 45 if maxT is not None: 46 maxTimeList.append(float(maxT)) 47 48 try: 49 resolvedMinT = min(minTimeList) 50 resolvedMaxT = max(maxTimeList) 51 except ValueError: 52 raise errors.TimelessTextgridTierException() 53 54 return (resolvedMinT, resolvedMaxT) 55 56 57class IntervalTier(textgrid_tier.TextgridTier): 58 tierType = INTERVAL_TIER 59 entryType = Interval 60 61 def __init__( 62 self, 63 name: str, 64 entries: List[Interval], 65 minT: Optional[float] = None, 66 maxT: Optional[float] = None, 67 ): 68 """An interval tier is for annotating events that have duration 69 70 The entries is of the form: 71 [(startTime1, endTime1, label1), (startTime2, endTime2, label2), ] 72 73 The data stored in the labels can be anything but will 74 be interpreted as text by praatio (the label could be descriptive 75 text e.g. ('erase this region') or numerical data e.g. (average pitch 76 values like '132')) 77 """ 78 entries = _homogenizeEntries(entries) 79 calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT) 80 81 super(IntervalTier, self).__init__( 82 name, entries, calculatedMinT, calculatedMaxT 83 ) 84 self._validate() 85 86 def _validate(self): 87 """An interval tier is invalid if the entries are out of order or overlap with each other""" 88 for entry in self.entries: 89 if entry.start >= entry.end: 90 raise errors.TextgridStateError( 91 f"The start time of an interval ({entry.start}) " 92 f"cannot occur after its end time ({entry.end})" 93 ) 94 95 for entry, nextEntry in zip(self.entries[0::], self.entries[1::]): 96 if entry.end > nextEntry.start: 97 raise errors.TextgridStateError( 98 "Two intervals in the same tier overlap in time:\n" 99 f"({entry.start}, {entry.end}, {entry.label}) and " 100 f"({nextEntry.start}, {nextEntry.end}, {nextEntry.label})" 101 ) 102 103 @property 104 def timestamps(self) -> List[float]: 105 """All unique timestamps used in this tier""" 106 tmpTimestamps = [ 107 time 108 for start, stop, _ in self.entries 109 for time in [ 110 start, 111 stop, 112 ] 113 ] 114 115 uniqueTimestamps = list(set(tmpTimestamps)) 116 uniqueTimestamps.sort() 117 118 return uniqueTimestamps 119 120 def crop( 121 self, 122 cropStart: float, 123 cropEnd: float, 124 mode: Literal["strict", "lax", "truncated"], 125 rebaseToZero: bool, 126 ) -> "IntervalTier": 127 """Creates a new tier with all entries that fit inside the new interval 128 129 Args: 130 cropStart: 131 cropEnd: 132 mode: determines cropping behavior 133 - 'strict', only intervals wholly contained by the crop 134 interval will be kept 135 - 'lax', partially contained intervals will be kept 136 - 'truncated', partially contained intervals will be 137 truncated to fit within the crop region. 138 rebaseToZero: if True, the cropped textgrid values 139 will be subtracted by the cropStart 140 141 Returns: 142 the modified version of the current tier 143 """ 144 145 utils.validateOption("mode", mode, CropCollision) 146 147 if cropStart >= cropEnd: 148 raise errors.ArgumentError( 149 f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})" 150 ) 151 152 newEntryList = utils.getIntervalsInInterval( 153 cropStart, cropEnd, self.entries, mode 154 ) 155 156 if rebaseToZero is True: 157 newSmallestValue = newEntryList[0][0] 158 if newSmallestValue < cropStart: 159 timeDiff = newSmallestValue 160 else: 161 timeDiff = cropStart 162 newEntryList = [ 163 Interval(start - timeDiff, end - timeDiff, label) 164 for start, end, label in newEntryList 165 ] 166 minT = 0.0 167 maxT = cropEnd - cropStart 168 else: 169 minT = cropStart 170 maxT = cropEnd 171 172 croppedTier = IntervalTier(self.name, newEntryList, minT, maxT) 173 174 return croppedTier 175 176 def dejitter( 177 self, 178 referenceTier: textgrid_tier.TextgridTier, 179 maxDifference: float = 0.001, 180 ) -> textgrid_tier.TextgridTier: 181 """ 182 Set timestamps in this tier to be the same as values in the reference tier 183 184 Timestamps will only be moved if they are less than maxDifference away from the 185 reference time. 186 187 This can be used to correct minor alignment errors between tiers, as made when 188 annotating files manually, etc. 189 190 Args: 191 referenceTier: the IntervalTier or PointTier to use as a reference 192 maxDifference: the maximum amount to allow timestamps to be moved by 193 194 Returns: 195 the modified version of the current tier 196 """ 197 referenceTimestamps = referenceTier.timestamps 198 199 newEntries = [] 200 for start, stop, label in self.entries: 201 startCompare = min(referenceTimestamps, key=lambda x: abs(x - start)) 202 stopCompare = min(referenceTimestamps, key=lambda x: abs(x - stop)) 203 204 if my_math.lessThanOrEqual(abs(start - startCompare), maxDifference): 205 start = startCompare 206 if my_math.lessThanOrEqual(abs(stop - stopCompare), maxDifference): 207 stop = stopCompare 208 newEntries.append((start, stop, label)) 209 210 return self.new(entries=newEntries) 211 212 def deleteEntry(self, entry: Interval) -> None: 213 """Removes an entry from the entries""" 214 self._entries.pop(self._entries.index(entry)) 215 216 def difference(self, tier: "IntervalTier") -> "IntervalTier": 217 """Takes the set difference of this tier and the given one 218 219 Any overlapping portions of entries with entries in this textgrid 220 will be removed from the returned tier. 221 222 Args: 223 tier: the tier to subtract from this one 224 225 Returns: 226 the modified version of the current tier 227 """ 228 retTier = self.new() 229 230 for entry in tier.entries: 231 retTier = retTier.eraseRegion( 232 entry.start, 233 entry.end, 234 collisionMode=constants.EraseCollision.TRUNCATE, 235 doShrink=False, 236 ) 237 238 return retTier 239 240 def editTimestamps( 241 self, 242 offset: float, 243 reportingMode: Literal["silence", "warning", "error"] = "warning", 244 ) -> "IntervalTier": 245 """Modifies all timestamps by a constant amount 246 247 Args: 248 offset: the amount to shift all intervals 249 reportingMode: Determines the behavior if an entries moves outside 250 of minTimestamp or maxTimestamp after being edited 251 252 Returns: 253 the modified version of the current tier 254 """ 255 utils.validateOption( 256 "reportingMode", reportingMode, constants.ErrorReportingMode 257 ) 258 errorReporter = utils.getErrorReporter(reportingMode) 259 260 newEntryList = [] 261 for interval in self.entries: 262 newStart = offset + interval.start 263 newEnd = offset + interval.end 264 265 utils.checkIsUndershoot(newStart, self.minTimestamp, errorReporter) 266 utils.checkIsOvershoot(newEnd, self.maxTimestamp, errorReporter) 267 268 if newEnd <= 0: 269 continue 270 if newStart < 0: 271 newStart = 0 272 273 newEntryList.append(Interval(newStart, newEnd, interval.label)) 274 275 # Determine new min and max timestamps 276 newMin = min([interval.start for interval in newEntryList]) 277 newMax = max([interval.end for interval in newEntryList]) 278 279 if newMin > self.minTimestamp: 280 newMin = self.minTimestamp 281 282 if newMax < self.maxTimestamp: 283 newMax = self.maxTimestamp 284 285 return IntervalTier(self.name, newEntryList, newMin, newMax) 286 287 def eraseRegion( 288 self, 289 start: float, 290 end: float, 291 collisionMode: Literal["truncate", "categorical", "error"] = "error", 292 doShrink: bool = True, 293 ) -> "IntervalTier": 294 """Makes a region in a tier blank (removes all contained entries) 295 296 Args: 297 start: 298 end: 299 collisionMode: Determines the behavior when the region to erase 300 overlaps with existing intervals. 301 - 'truncate' partially contained entries will have the portion 302 removed that overlaps with the target entry 303 - 'categorical' all entries that overlap, even partially, with 304 the target entry will be completely removed 305 - None or any other value throws IntervalCollision 306 doShrink: If True, moves leftward by (/end/ - /start/) 307 amount, each item that occurs after /end/ 308 309 Returns: 310 The modified version of the current tier 311 312 Raises: 313 CollisionError 314 """ 315 utils.validateOption("collisionMode", collisionMode, constants.EraseCollision) 316 317 matchList = self.crop(start, end, CropCollision.LAX, False).entries 318 newTier = self.new() 319 320 if len(matchList) == 0: 321 pass 322 else: 323 if collisionMode == constants.EraseCollision.ERROR: 324 raise errors.CollisionError( 325 f"Erase region ({start}, {end})overlapped with an interval. " 326 "If this was expected, consider setting the collisionMode" 327 ) 328 329 # Remove all the matches from the entries 330 # Go in reverse order because we're destructively altering 331 # the order of the list (messes up index order) 332 for interval in matchList[::-1]: 333 newTier.deleteEntry(interval) 334 335 # If we're only truncating, reinsert entries on the left and 336 # right edges 337 # if categorical, it doesn't make it into the list at all 338 if collisionMode == constants.EraseCollision.TRUNCATE: 339 # Check left edge 340 if matchList[0].start < start: 341 newEntry = Interval(matchList[0].start, start, matchList[0].label) 342 newTier.insertEntry(newEntry) 343 344 # Check right edge 345 if matchList[-1].end > end: 346 newEntry = Interval(end, matchList[-1].end, matchList[-1].label) 347 newTier.insertEntry(newEntry) 348 349 if doShrink is True: 350 diff = end - start 351 newEntryList = [] 352 for interval in newTier.entries: 353 if interval.end <= start: 354 newEntryList.append(interval) 355 elif interval.start >= end: 356 newEntryList.append( 357 Interval( 358 interval.start - diff, interval.end - diff, interval.label 359 ) 360 ) 361 362 # Special case: an interval that spanned the deleted 363 # section 364 for i in range(0, len(newEntryList) - 1): 365 rightEdge = newEntryList[i].end == start 366 leftEdge = newEntryList[i + 1].start == start 367 sameLabel = newEntryList[i].label == newEntryList[i + 1].label 368 if rightEdge and leftEdge and sameLabel: 369 newInterval = Interval( 370 newEntryList[i].start, 371 newEntryList[i + 1].end, 372 newEntryList[i].label, 373 ) 374 375 newEntryList.pop(i + 1) 376 newEntryList.pop(i) 377 newEntryList.insert(i, newInterval) 378 379 # Only one interval can span the deleted section, 380 # so if we've found it, move on 381 break 382 383 newMax = newTier.maxTimestamp - diff 384 newTier = newTier.new(entries=newEntryList, maxTimestamp=newMax) 385 386 return newTier 387 388 def getValuesInIntervals(self, dataTupleList: List) -> List[Tuple[Interval, List]]: 389 """Returns data from dataTupleList contained in labeled intervals 390 391 Each labeled interval will get its own list of data values. 392 393 dataTupleList should be of the form: 394 [(time1, value1a, value1b,...), (time2, value2a, value2b...), ...] 395 """ 396 397 returnList = [] 398 399 for interval in self.entries: 400 intervalDataList = utils.getValuesInInterval( 401 dataTupleList, interval.start, interval.end 402 ) 403 returnList.append((interval, intervalDataList)) 404 405 return returnList 406 407 def getNonEntries(self) -> List[Interval]: 408 """Returns the regions of the textgrid without labels 409 410 This can include unlabeled segments and regions marked as silent. 411 """ 412 entries = self.entries 413 invertedEntryList = [ 414 Interval(entries[i].end, entries[i + 1].start, "") 415 for i in range(len(entries) - 1) 416 ] 417 418 # Remove entries that have no duration (ie lie between two entries 419 # that share a border) 420 invertedEntryList = [ 421 interval for interval in invertedEntryList if interval.start < interval.end 422 ] 423 424 if entries[0].start > 0: 425 invertedEntryList.insert(0, Interval(0, entries[0].start, "")) 426 427 if entries[-1].end < self.maxTimestamp: 428 invertedEntryList.append(Interval(entries[-1].end, self.maxTimestamp, "")) 429 430 invertedEntryList = [ 431 interval if isinstance(interval, Interval) else Interval(*interval) 432 for interval in invertedEntryList 433 ] 434 435 return invertedEntryList 436 437 def insertEntry( 438 self, 439 entry: Interval, 440 collisionMode: Literal["replace", "merge", "error"] = "error", 441 collisionReportingMode: Literal["silence", "warning"] = "warning", 442 ) -> None: 443 """Inserts an interval into the tier 444 445 Args: 446 entry: the Interval to insert 447 collisionMode: determines the behavior in the event that intervals 448 exist in the insertion area. 449 - 'replace' will remove existing items 450 - 'merge' will fuse the inserting item with existing items 451 - None or any other value will throw a CollisionError 452 collisionReportingMode: Determines the behavior if the new entry 453 overlaps with an existing one 454 455 Returns: 456 the modified version of the current tier 457 """ 458 utils.validateOption( 459 "collisionMode", collisionMode, constants.IntervalCollision 460 ) 461 utils.validateOption( 462 "collisionReportingMode", 463 collisionReportingMode, 464 constants.ErrorReportingMode, 465 ) 466 collisionReporter = utils.getErrorReporter(collisionReportingMode) 467 468 if not isinstance(entry, Interval): 469 interval = Interval(*entry) 470 else: 471 interval = entry 472 473 matchList = self.crop( 474 interval.start, interval.end, CropCollision.LAX, False 475 )._entries 476 477 if len(matchList) == 0: 478 self._entries.append(interval) 479 480 elif collisionMode == constants.IntervalCollision.REPLACE: 481 for matchEntry in matchList: 482 self.deleteEntry(matchEntry) 483 self._entries.append(interval) 484 485 elif collisionMode == constants.IntervalCollision.MERGE: 486 for matchEntry in matchList: 487 self.deleteEntry(matchEntry) 488 matchList.append(interval) 489 matchList.sort() # By starting time 490 491 newInterval = Interval( 492 min([tmpInterval.start for tmpInterval in matchList]), 493 max([tmpInterval.end for tmpInterval in matchList]), 494 "-".join([tmpInterval.label for tmpInterval in matchList]), 495 ) 496 self._entries.append(newInterval) 497 498 else: 499 raise errors.CollisionError( 500 "Attempted to insert interval " 501 f"({interval.start}, {interval.end}, '{interval.label}') into tier {self.name} " 502 "of textgrid but overlapping entries " 503 f"{[tuple(interval) for interval in matchList]} " 504 "already exist" 505 ) 506 507 self.sort() 508 509 if self._entries[0][0] < self.minTimestamp: 510 self.minTimestamp = self._entries[0][0] 511 512 if self._entries[-1][1] > self.maxTimestamp: 513 self.maxTimestamp = self._entries[-1][1] 514 515 if len(matchList) != 0: 516 collisionReporter( 517 errors.CollisionError, 518 f"Collision warning for ({interval}) with items " 519 f"({matchList}) of tier '{self.name}'", 520 ) 521 522 def insertSpace( 523 self, 524 start: float, 525 duration: float, 526 collisionMode: Literal["stretch", "split", "no_change", "error"], 527 ) -> "IntervalTier": 528 """Inserts a blank region into the tier 529 530 Args: 531 start: 532 duration: 533 collisionMode: Determines the behavior that occurs if 534 an interval stradles the starting point 535 - 'stretch' stretches the interval by /duration/ amount 536 - 'split' splits the interval into two--everything to the 537 right of 'start' will be advanced by 'duration' seconds 538 - 'no change' leaves the interval as is with no change 539 - 'error' will stop execution and raise an error 540 541 Returns: 542 the modified version of the current tier 543 """ 544 utils.validateOption( 545 "collisionMode", collisionMode, constants.WhitespaceCollision 546 ) 547 548 newEntryList = [] 549 for interval in self.entries: 550 # Entry exists before the insertion point 551 if interval.end <= start: 552 newEntryList.append(interval) 553 # Entry exists after the insertion point 554 elif interval.start >= start: 555 newEntryList.append( 556 Interval( 557 interval.start + duration, 558 interval.end + duration, 559 interval.label, 560 ) 561 ) 562 # Entry straddles the insertion point 563 elif interval.start <= start and interval.end > start: 564 if collisionMode == constants.WhitespaceCollision.STRETCH: 565 newEntryList.append( 566 Interval( 567 interval.start, interval.end + duration, interval.label 568 ) 569 ) 570 elif collisionMode == constants.WhitespaceCollision.SPLIT: 571 # Left side of the split 572 newEntryList.append(Interval(interval.start, start, interval.label)) 573 # Right side of the split 574 newEntryList.append( 575 ( 576 start + duration, 577 start + duration + (interval.end - start), 578 interval.label, 579 ) 580 ) 581 elif collisionMode == constants.WhitespaceCollision.NO_CHANGE: 582 newEntryList.append(interval) 583 else: 584 raise errors.ArgumentError( 585 f"Collision occured during insertSpace() for interval '{interval}' " 586 f"and given white space insertion interval ({start}, {start + duration})" 587 ) 588 589 newTier = self.new( 590 entries=newEntryList, maxTimestamp=self.maxTimestamp + duration 591 ) 592 593 return newTier 594 595 def intersection(self, tier: "IntervalTier", demarcator="-") -> "IntervalTier": 596 """Takes the set intersection of this tier and the given one 597 598 - The output will contain one interval for each overlapping pair 599 e.g. [(1, 2, 'foo')] and [(1, 1.3, 'bang'), (1.7, 2, 'wizz')] 600 -> [(1, 1.3, 'foo-bang'), (1.7, 2, 'foo-wizz')] 601 - Only intervals that exist in both tiers will remain in the returned tier. 602 e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] 603 -> [(1, 2, 'foo-bang')] 604 - If intervals partially overlap, only the overlapping portion will be returned. 605 e.g. [(1, 2, 'foo')] and [(0.5, 1.5, 'bang')] 606 -> [(1, 1.5, 'foo-bang')] 607 608 Compare with IntervalTier.mergeLabels 609 610 Args: 611 tier: the tier to intersect with 612 demarcator: the character to separate the labels of the overlapping intervals 613 614 Returns: 615 IntervalTier: the modified version of the current tier 616 """ 617 retEntryList = [] 618 for interval in tier.entries: 619 subTier = self.crop( 620 interval.start, interval.end, CropCollision.TRUNCATED, False 621 ) 622 623 # Combine the labels in the two tiers 624 subEntryList = [ 625 ( 626 subInterval.start, 627 subInterval.end, 628 f"{subInterval.label}{demarcator}{interval.label}", 629 ) 630 for subInterval in subTier.entries 631 ] 632 633 retEntryList.extend(subEntryList) 634 635 newName = f"{self.name}-{tier.name}" 636 637 retTier = self.new(newName, retEntryList) 638 639 return retTier 640 641 def mergeLabels( 642 self, tier: "IntervalTier", demarcator: str = "," 643 ) -> "IntervalTier": 644 """Merges labels of overlapping tiers into this tier 645 646 - All intervals in this tier will appear in the output; for the given tier, only intervals 647 that overlap with content in this tier will appear in the output 648 e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] 649 -> [(1, 2, 'foo(bang)'), (3, 4, 'bar()')] 650 - If multiple entries exist in a subinterval, their labels will be concatenated 651 e.g. [(1, 2, 'hi')] and [(1, 1.5, 'h'), (1.5, 2, 'ai')] -> [(1, 2, 'hi(h,ai)')] 652 653 compare with IntervalTier.intersection 654 655 Args: 656 tier: the tier to intersect with 657 demarcator: the string to separate items that fall in the same subinterval 658 659 Returns: 660 IntervalTier: the modified version of the current tier 661 """ 662 retEntryList = [] 663 for interval in self.entries: 664 subTier = tier.crop( 665 interval.start, interval.end, CropCollision.TRUNCATED, False 666 ) 667 if len(subTier._entries) == 0: 668 continue 669 670 subLabel = demarcator.join([entry.label for entry in subTier.entries]) 671 label = f"{interval.label}({subLabel})" 672 673 start = min(interval.start, subTier._entries[0].start) 674 end = max(interval.end, subTier._entries[-1].end) 675 676 intersectedInterval = ( 677 start, 678 end, 679 label, 680 ) 681 682 retEntryList.append(intersectedInterval) 683 684 newName = f"{self.name}-{tier.name}" 685 686 retTier = self.new(newName, retEntryList) 687 688 return retTier 689 690 def morph( 691 self, 692 targetTier: "IntervalTier", 693 filterFunc: Optional[Callable[[str], bool]] = None, 694 ) -> "IntervalTier": 695 """Morphs the duration of segments in this tier to those in another 696 697 This preserves the labels and the duration of silence in 698 this tier while changing the duration of labeled segments. 699 700 Args: 701 targetTier: 702 filterFunc: if specified, filters entries. The 703 functor takes one argument, an Interval. It returns true 704 if the Interval should be modified and false if not. 705 706 Returns: 707 The modified version of the current tier 708 """ 709 cumulativeAdjustAmount = 0 710 newEntryList = [] 711 allIntervals = [self.entries, targetTier.entries] 712 for sourceInterval, targetInterval in utils.safeZip(allIntervals, True): 713 # sourceInterval.start - lastFromEnd -> was this interval and the 714 # last one adjacent? 715 newStart = sourceInterval.start + cumulativeAdjustAmount 716 717 currIntervalDuration = sourceInterval.end - sourceInterval.start 718 if filterFunc is None or filterFunc(sourceInterval.label): 719 newIntervalDuration = targetInterval.end - targetInterval.start 720 cumulativeAdjustAmount += newIntervalDuration - currIntervalDuration 721 newEnd = newStart + newIntervalDuration 722 else: 723 newEnd = newStart + currIntervalDuration 724 725 newEntryList.append(Interval(newStart, newEnd, sourceInterval.label)) 726 727 newMin = self.minTimestamp 728 cumulativeDifference = newEntryList[-1].end - self.entries[-1].end 729 newMax = self.maxTimestamp + cumulativeDifference 730 731 return IntervalTier(self.name, newEntryList, newMin, newMax) 732 733 def validate( 734 self, reportingMode: Literal["silence", "warning", "error"] = "warning" 735 ) -> bool: 736 """Validate this tier 737 738 Args: 739 reportingMode (str): Determines the behavior if validation fails. 740 741 Returns: 742 True if the tier is valid; False if not 743 """ 744 utils.validateOption( 745 "reportingMode", reportingMode, constants.ErrorReportingMode 746 ) 747 errorReporter = utils.getErrorReporter(reportingMode) 748 749 isValid = True 750 previousInterval = None 751 for interval in self.entries: 752 if interval.start >= interval.end: 753 isValid = False 754 errorReporter( 755 errors.TextgridStateError, 756 f"Invalid interval. End time occurs before or on the start time({interval}).", 757 ) 758 759 if previousInterval and previousInterval.end > interval.start: 760 isValid = False 761 errorReporter( 762 errors.TextgridStateError, 763 f"Intervals are not sorted in time: " 764 f"[({previousInterval}), ({interval})]", 765 ) 766 767 if utils.checkIsUndershoot( 768 interval.start, self.minTimestamp, errorReporter 769 ): 770 isValid = False 771 772 if utils.checkIsOvershoot(interval.end, self.maxTimestamp, errorReporter): 773 isValid = False 774 775 previousInterval = interval 776 777 return isValid
58class IntervalTier(textgrid_tier.TextgridTier): 59 tierType = INTERVAL_TIER 60 entryType = Interval 61 62 def __init__( 63 self, 64 name: str, 65 entries: List[Interval], 66 minT: Optional[float] = None, 67 maxT: Optional[float] = None, 68 ): 69 """An interval tier is for annotating events that have duration 70 71 The entries is of the form: 72 [(startTime1, endTime1, label1), (startTime2, endTime2, label2), ] 73 74 The data stored in the labels can be anything but will 75 be interpreted as text by praatio (the label could be descriptive 76 text e.g. ('erase this region') or numerical data e.g. (average pitch 77 values like '132')) 78 """ 79 entries = _homogenizeEntries(entries) 80 calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT) 81 82 super(IntervalTier, self).__init__( 83 name, entries, calculatedMinT, calculatedMaxT 84 ) 85 self._validate() 86 87 def _validate(self): 88 """An interval tier is invalid if the entries are out of order or overlap with each other""" 89 for entry in self.entries: 90 if entry.start >= entry.end: 91 raise errors.TextgridStateError( 92 f"The start time of an interval ({entry.start}) " 93 f"cannot occur after its end time ({entry.end})" 94 ) 95 96 for entry, nextEntry in zip(self.entries[0::], self.entries[1::]): 97 if entry.end > nextEntry.start: 98 raise errors.TextgridStateError( 99 "Two intervals in the same tier overlap in time:\n" 100 f"({entry.start}, {entry.end}, {entry.label}) and " 101 f"({nextEntry.start}, {nextEntry.end}, {nextEntry.label})" 102 ) 103 104 @property 105 def timestamps(self) -> List[float]: 106 """All unique timestamps used in this tier""" 107 tmpTimestamps = [ 108 time 109 for start, stop, _ in self.entries 110 for time in [ 111 start, 112 stop, 113 ] 114 ] 115 116 uniqueTimestamps = list(set(tmpTimestamps)) 117 uniqueTimestamps.sort() 118 119 return uniqueTimestamps 120 121 def crop( 122 self, 123 cropStart: float, 124 cropEnd: float, 125 mode: Literal["strict", "lax", "truncated"], 126 rebaseToZero: bool, 127 ) -> "IntervalTier": 128 """Creates a new tier with all entries that fit inside the new interval 129 130 Args: 131 cropStart: 132 cropEnd: 133 mode: determines cropping behavior 134 - 'strict', only intervals wholly contained by the crop 135 interval will be kept 136 - 'lax', partially contained intervals will be kept 137 - 'truncated', partially contained intervals will be 138 truncated to fit within the crop region. 139 rebaseToZero: if True, the cropped textgrid values 140 will be subtracted by the cropStart 141 142 Returns: 143 the modified version of the current tier 144 """ 145 146 utils.validateOption("mode", mode, CropCollision) 147 148 if cropStart >= cropEnd: 149 raise errors.ArgumentError( 150 f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})" 151 ) 152 153 newEntryList = utils.getIntervalsInInterval( 154 cropStart, cropEnd, self.entries, mode 155 ) 156 157 if rebaseToZero is True: 158 newSmallestValue = newEntryList[0][0] 159 if newSmallestValue < cropStart: 160 timeDiff = newSmallestValue 161 else: 162 timeDiff = cropStart 163 newEntryList = [ 164 Interval(start - timeDiff, end - timeDiff, label) 165 for start, end, label in newEntryList 166 ] 167 minT = 0.0 168 maxT = cropEnd - cropStart 169 else: 170 minT = cropStart 171 maxT = cropEnd 172 173 croppedTier = IntervalTier(self.name, newEntryList, minT, maxT) 174 175 return croppedTier 176 177 def dejitter( 178 self, 179 referenceTier: textgrid_tier.TextgridTier, 180 maxDifference: float = 0.001, 181 ) -> textgrid_tier.TextgridTier: 182 """ 183 Set timestamps in this tier to be the same as values in the reference tier 184 185 Timestamps will only be moved if they are less than maxDifference away from the 186 reference time. 187 188 This can be used to correct minor alignment errors between tiers, as made when 189 annotating files manually, etc. 190 191 Args: 192 referenceTier: the IntervalTier or PointTier to use as a reference 193 maxDifference: the maximum amount to allow timestamps to be moved by 194 195 Returns: 196 the modified version of the current tier 197 """ 198 referenceTimestamps = referenceTier.timestamps 199 200 newEntries = [] 201 for start, stop, label in self.entries: 202 startCompare = min(referenceTimestamps, key=lambda x: abs(x - start)) 203 stopCompare = min(referenceTimestamps, key=lambda x: abs(x - stop)) 204 205 if my_math.lessThanOrEqual(abs(start - startCompare), maxDifference): 206 start = startCompare 207 if my_math.lessThanOrEqual(abs(stop - stopCompare), maxDifference): 208 stop = stopCompare 209 newEntries.append((start, stop, label)) 210 211 return self.new(entries=newEntries) 212 213 def deleteEntry(self, entry: Interval) -> None: 214 """Removes an entry from the entries""" 215 self._entries.pop(self._entries.index(entry)) 216 217 def difference(self, tier: "IntervalTier") -> "IntervalTier": 218 """Takes the set difference of this tier and the given one 219 220 Any overlapping portions of entries with entries in this textgrid 221 will be removed from the returned tier. 222 223 Args: 224 tier: the tier to subtract from this one 225 226 Returns: 227 the modified version of the current tier 228 """ 229 retTier = self.new() 230 231 for entry in tier.entries: 232 retTier = retTier.eraseRegion( 233 entry.start, 234 entry.end, 235 collisionMode=constants.EraseCollision.TRUNCATE, 236 doShrink=False, 237 ) 238 239 return retTier 240 241 def editTimestamps( 242 self, 243 offset: float, 244 reportingMode: Literal["silence", "warning", "error"] = "warning", 245 ) -> "IntervalTier": 246 """Modifies all timestamps by a constant amount 247 248 Args: 249 offset: the amount to shift all intervals 250 reportingMode: Determines the behavior if an entries moves outside 251 of minTimestamp or maxTimestamp after being edited 252 253 Returns: 254 the modified version of the current tier 255 """ 256 utils.validateOption( 257 "reportingMode", reportingMode, constants.ErrorReportingMode 258 ) 259 errorReporter = utils.getErrorReporter(reportingMode) 260 261 newEntryList = [] 262 for interval in self.entries: 263 newStart = offset + interval.start 264 newEnd = offset + interval.end 265 266 utils.checkIsUndershoot(newStart, self.minTimestamp, errorReporter) 267 utils.checkIsOvershoot(newEnd, self.maxTimestamp, errorReporter) 268 269 if newEnd <= 0: 270 continue 271 if newStart < 0: 272 newStart = 0 273 274 newEntryList.append(Interval(newStart, newEnd, interval.label)) 275 276 # Determine new min and max timestamps 277 newMin = min([interval.start for interval in newEntryList]) 278 newMax = max([interval.end for interval in newEntryList]) 279 280 if newMin > self.minTimestamp: 281 newMin = self.minTimestamp 282 283 if newMax < self.maxTimestamp: 284 newMax = self.maxTimestamp 285 286 return IntervalTier(self.name, newEntryList, newMin, newMax) 287 288 def eraseRegion( 289 self, 290 start: float, 291 end: float, 292 collisionMode: Literal["truncate", "categorical", "error"] = "error", 293 doShrink: bool = True, 294 ) -> "IntervalTier": 295 """Makes a region in a tier blank (removes all contained entries) 296 297 Args: 298 start: 299 end: 300 collisionMode: Determines the behavior when the region to erase 301 overlaps with existing intervals. 302 - 'truncate' partially contained entries will have the portion 303 removed that overlaps with the target entry 304 - 'categorical' all entries that overlap, even partially, with 305 the target entry will be completely removed 306 - None or any other value throws IntervalCollision 307 doShrink: If True, moves leftward by (/end/ - /start/) 308 amount, each item that occurs after /end/ 309 310 Returns: 311 The modified version of the current tier 312 313 Raises: 314 CollisionError 315 """ 316 utils.validateOption("collisionMode", collisionMode, constants.EraseCollision) 317 318 matchList = self.crop(start, end, CropCollision.LAX, False).entries 319 newTier = self.new() 320 321 if len(matchList) == 0: 322 pass 323 else: 324 if collisionMode == constants.EraseCollision.ERROR: 325 raise errors.CollisionError( 326 f"Erase region ({start}, {end})overlapped with an interval. " 327 "If this was expected, consider setting the collisionMode" 328 ) 329 330 # Remove all the matches from the entries 331 # Go in reverse order because we're destructively altering 332 # the order of the list (messes up index order) 333 for interval in matchList[::-1]: 334 newTier.deleteEntry(interval) 335 336 # If we're only truncating, reinsert entries on the left and 337 # right edges 338 # if categorical, it doesn't make it into the list at all 339 if collisionMode == constants.EraseCollision.TRUNCATE: 340 # Check left edge 341 if matchList[0].start < start: 342 newEntry = Interval(matchList[0].start, start, matchList[0].label) 343 newTier.insertEntry(newEntry) 344 345 # Check right edge 346 if matchList[-1].end > end: 347 newEntry = Interval(end, matchList[-1].end, matchList[-1].label) 348 newTier.insertEntry(newEntry) 349 350 if doShrink is True: 351 diff = end - start 352 newEntryList = [] 353 for interval in newTier.entries: 354 if interval.end <= start: 355 newEntryList.append(interval) 356 elif interval.start >= end: 357 newEntryList.append( 358 Interval( 359 interval.start - diff, interval.end - diff, interval.label 360 ) 361 ) 362 363 # Special case: an interval that spanned the deleted 364 # section 365 for i in range(0, len(newEntryList) - 1): 366 rightEdge = newEntryList[i].end == start 367 leftEdge = newEntryList[i + 1].start == start 368 sameLabel = newEntryList[i].label == newEntryList[i + 1].label 369 if rightEdge and leftEdge and sameLabel: 370 newInterval = Interval( 371 newEntryList[i].start, 372 newEntryList[i + 1].end, 373 newEntryList[i].label, 374 ) 375 376 newEntryList.pop(i + 1) 377 newEntryList.pop(i) 378 newEntryList.insert(i, newInterval) 379 380 # Only one interval can span the deleted section, 381 # so if we've found it, move on 382 break 383 384 newMax = newTier.maxTimestamp - diff 385 newTier = newTier.new(entries=newEntryList, maxTimestamp=newMax) 386 387 return newTier 388 389 def getValuesInIntervals(self, dataTupleList: List) -> List[Tuple[Interval, List]]: 390 """Returns data from dataTupleList contained in labeled intervals 391 392 Each labeled interval will get its own list of data values. 393 394 dataTupleList should be of the form: 395 [(time1, value1a, value1b,...), (time2, value2a, value2b...), ...] 396 """ 397 398 returnList = [] 399 400 for interval in self.entries: 401 intervalDataList = utils.getValuesInInterval( 402 dataTupleList, interval.start, interval.end 403 ) 404 returnList.append((interval, intervalDataList)) 405 406 return returnList 407 408 def getNonEntries(self) -> List[Interval]: 409 """Returns the regions of the textgrid without labels 410 411 This can include unlabeled segments and regions marked as silent. 412 """ 413 entries = self.entries 414 invertedEntryList = [ 415 Interval(entries[i].end, entries[i + 1].start, "") 416 for i in range(len(entries) - 1) 417 ] 418 419 # Remove entries that have no duration (ie lie between two entries 420 # that share a border) 421 invertedEntryList = [ 422 interval for interval in invertedEntryList if interval.start < interval.end 423 ] 424 425 if entries[0].start > 0: 426 invertedEntryList.insert(0, Interval(0, entries[0].start, "")) 427 428 if entries[-1].end < self.maxTimestamp: 429 invertedEntryList.append(Interval(entries[-1].end, self.maxTimestamp, "")) 430 431 invertedEntryList = [ 432 interval if isinstance(interval, Interval) else Interval(*interval) 433 for interval in invertedEntryList 434 ] 435 436 return invertedEntryList 437 438 def insertEntry( 439 self, 440 entry: Interval, 441 collisionMode: Literal["replace", "merge", "error"] = "error", 442 collisionReportingMode: Literal["silence", "warning"] = "warning", 443 ) -> None: 444 """Inserts an interval into the tier 445 446 Args: 447 entry: the Interval to insert 448 collisionMode: determines the behavior in the event that intervals 449 exist in the insertion area. 450 - 'replace' will remove existing items 451 - 'merge' will fuse the inserting item with existing items 452 - None or any other value will throw a CollisionError 453 collisionReportingMode: Determines the behavior if the new entry 454 overlaps with an existing one 455 456 Returns: 457 the modified version of the current tier 458 """ 459 utils.validateOption( 460 "collisionMode", collisionMode, constants.IntervalCollision 461 ) 462 utils.validateOption( 463 "collisionReportingMode", 464 collisionReportingMode, 465 constants.ErrorReportingMode, 466 ) 467 collisionReporter = utils.getErrorReporter(collisionReportingMode) 468 469 if not isinstance(entry, Interval): 470 interval = Interval(*entry) 471 else: 472 interval = entry 473 474 matchList = self.crop( 475 interval.start, interval.end, CropCollision.LAX, False 476 )._entries 477 478 if len(matchList) == 0: 479 self._entries.append(interval) 480 481 elif collisionMode == constants.IntervalCollision.REPLACE: 482 for matchEntry in matchList: 483 self.deleteEntry(matchEntry) 484 self._entries.append(interval) 485 486 elif collisionMode == constants.IntervalCollision.MERGE: 487 for matchEntry in matchList: 488 self.deleteEntry(matchEntry) 489 matchList.append(interval) 490 matchList.sort() # By starting time 491 492 newInterval = Interval( 493 min([tmpInterval.start for tmpInterval in matchList]), 494 max([tmpInterval.end for tmpInterval in matchList]), 495 "-".join([tmpInterval.label for tmpInterval in matchList]), 496 ) 497 self._entries.append(newInterval) 498 499 else: 500 raise errors.CollisionError( 501 "Attempted to insert interval " 502 f"({interval.start}, {interval.end}, '{interval.label}') into tier {self.name} " 503 "of textgrid but overlapping entries " 504 f"{[tuple(interval) for interval in matchList]} " 505 "already exist" 506 ) 507 508 self.sort() 509 510 if self._entries[0][0] < self.minTimestamp: 511 self.minTimestamp = self._entries[0][0] 512 513 if self._entries[-1][1] > self.maxTimestamp: 514 self.maxTimestamp = self._entries[-1][1] 515 516 if len(matchList) != 0: 517 collisionReporter( 518 errors.CollisionError, 519 f"Collision warning for ({interval}) with items " 520 f"({matchList}) of tier '{self.name}'", 521 ) 522 523 def insertSpace( 524 self, 525 start: float, 526 duration: float, 527 collisionMode: Literal["stretch", "split", "no_change", "error"], 528 ) -> "IntervalTier": 529 """Inserts a blank region into the tier 530 531 Args: 532 start: 533 duration: 534 collisionMode: Determines the behavior that occurs if 535 an interval stradles the starting point 536 - 'stretch' stretches the interval by /duration/ amount 537 - 'split' splits the interval into two--everything to the 538 right of 'start' will be advanced by 'duration' seconds 539 - 'no change' leaves the interval as is with no change 540 - 'error' will stop execution and raise an error 541 542 Returns: 543 the modified version of the current tier 544 """ 545 utils.validateOption( 546 "collisionMode", collisionMode, constants.WhitespaceCollision 547 ) 548 549 newEntryList = [] 550 for interval in self.entries: 551 # Entry exists before the insertion point 552 if interval.end <= start: 553 newEntryList.append(interval) 554 # Entry exists after the insertion point 555 elif interval.start >= start: 556 newEntryList.append( 557 Interval( 558 interval.start + duration, 559 interval.end + duration, 560 interval.label, 561 ) 562 ) 563 # Entry straddles the insertion point 564 elif interval.start <= start and interval.end > start: 565 if collisionMode == constants.WhitespaceCollision.STRETCH: 566 newEntryList.append( 567 Interval( 568 interval.start, interval.end + duration, interval.label 569 ) 570 ) 571 elif collisionMode == constants.WhitespaceCollision.SPLIT: 572 # Left side of the split 573 newEntryList.append(Interval(interval.start, start, interval.label)) 574 # Right side of the split 575 newEntryList.append( 576 ( 577 start + duration, 578 start + duration + (interval.end - start), 579 interval.label, 580 ) 581 ) 582 elif collisionMode == constants.WhitespaceCollision.NO_CHANGE: 583 newEntryList.append(interval) 584 else: 585 raise errors.ArgumentError( 586 f"Collision occured during insertSpace() for interval '{interval}' " 587 f"and given white space insertion interval ({start}, {start + duration})" 588 ) 589 590 newTier = self.new( 591 entries=newEntryList, maxTimestamp=self.maxTimestamp + duration 592 ) 593 594 return newTier 595 596 def intersection(self, tier: "IntervalTier", demarcator="-") -> "IntervalTier": 597 """Takes the set intersection of this tier and the given one 598 599 - The output will contain one interval for each overlapping pair 600 e.g. [(1, 2, 'foo')] and [(1, 1.3, 'bang'), (1.7, 2, 'wizz')] 601 -> [(1, 1.3, 'foo-bang'), (1.7, 2, 'foo-wizz')] 602 - Only intervals that exist in both tiers will remain in the returned tier. 603 e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] 604 -> [(1, 2, 'foo-bang')] 605 - If intervals partially overlap, only the overlapping portion will be returned. 606 e.g. [(1, 2, 'foo')] and [(0.5, 1.5, 'bang')] 607 -> [(1, 1.5, 'foo-bang')] 608 609 Compare with IntervalTier.mergeLabels 610 611 Args: 612 tier: the tier to intersect with 613 demarcator: the character to separate the labels of the overlapping intervals 614 615 Returns: 616 IntervalTier: the modified version of the current tier 617 """ 618 retEntryList = [] 619 for interval in tier.entries: 620 subTier = self.crop( 621 interval.start, interval.end, CropCollision.TRUNCATED, False 622 ) 623 624 # Combine the labels in the two tiers 625 subEntryList = [ 626 ( 627 subInterval.start, 628 subInterval.end, 629 f"{subInterval.label}{demarcator}{interval.label}", 630 ) 631 for subInterval in subTier.entries 632 ] 633 634 retEntryList.extend(subEntryList) 635 636 newName = f"{self.name}-{tier.name}" 637 638 retTier = self.new(newName, retEntryList) 639 640 return retTier 641 642 def mergeLabels( 643 self, tier: "IntervalTier", demarcator: str = "," 644 ) -> "IntervalTier": 645 """Merges labels of overlapping tiers into this tier 646 647 - All intervals in this tier will appear in the output; for the given tier, only intervals 648 that overlap with content in this tier will appear in the output 649 e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] 650 -> [(1, 2, 'foo(bang)'), (3, 4, 'bar()')] 651 - If multiple entries exist in a subinterval, their labels will be concatenated 652 e.g. [(1, 2, 'hi')] and [(1, 1.5, 'h'), (1.5, 2, 'ai')] -> [(1, 2, 'hi(h,ai)')] 653 654 compare with IntervalTier.intersection 655 656 Args: 657 tier: the tier to intersect with 658 demarcator: the string to separate items that fall in the same subinterval 659 660 Returns: 661 IntervalTier: the modified version of the current tier 662 """ 663 retEntryList = [] 664 for interval in self.entries: 665 subTier = tier.crop( 666 interval.start, interval.end, CropCollision.TRUNCATED, False 667 ) 668 if len(subTier._entries) == 0: 669 continue 670 671 subLabel = demarcator.join([entry.label for entry in subTier.entries]) 672 label = f"{interval.label}({subLabel})" 673 674 start = min(interval.start, subTier._entries[0].start) 675 end = max(interval.end, subTier._entries[-1].end) 676 677 intersectedInterval = ( 678 start, 679 end, 680 label, 681 ) 682 683 retEntryList.append(intersectedInterval) 684 685 newName = f"{self.name}-{tier.name}" 686 687 retTier = self.new(newName, retEntryList) 688 689 return retTier 690 691 def morph( 692 self, 693 targetTier: "IntervalTier", 694 filterFunc: Optional[Callable[[str], bool]] = None, 695 ) -> "IntervalTier": 696 """Morphs the duration of segments in this tier to those in another 697 698 This preserves the labels and the duration of silence in 699 this tier while changing the duration of labeled segments. 700 701 Args: 702 targetTier: 703 filterFunc: if specified, filters entries. The 704 functor takes one argument, an Interval. It returns true 705 if the Interval should be modified and false if not. 706 707 Returns: 708 The modified version of the current tier 709 """ 710 cumulativeAdjustAmount = 0 711 newEntryList = [] 712 allIntervals = [self.entries, targetTier.entries] 713 for sourceInterval, targetInterval in utils.safeZip(allIntervals, True): 714 # sourceInterval.start - lastFromEnd -> was this interval and the 715 # last one adjacent? 716 newStart = sourceInterval.start + cumulativeAdjustAmount 717 718 currIntervalDuration = sourceInterval.end - sourceInterval.start 719 if filterFunc is None or filterFunc(sourceInterval.label): 720 newIntervalDuration = targetInterval.end - targetInterval.start 721 cumulativeAdjustAmount += newIntervalDuration - currIntervalDuration 722 newEnd = newStart + newIntervalDuration 723 else: 724 newEnd = newStart + currIntervalDuration 725 726 newEntryList.append(Interval(newStart, newEnd, sourceInterval.label)) 727 728 newMin = self.minTimestamp 729 cumulativeDifference = newEntryList[-1].end - self.entries[-1].end 730 newMax = self.maxTimestamp + cumulativeDifference 731 732 return IntervalTier(self.name, newEntryList, newMin, newMax) 733 734 def validate( 735 self, reportingMode: Literal["silence", "warning", "error"] = "warning" 736 ) -> bool: 737 """Validate this tier 738 739 Args: 740 reportingMode (str): Determines the behavior if validation fails. 741 742 Returns: 743 True if the tier is valid; False if not 744 """ 745 utils.validateOption( 746 "reportingMode", reportingMode, constants.ErrorReportingMode 747 ) 748 errorReporter = utils.getErrorReporter(reportingMode) 749 750 isValid = True 751 previousInterval = None 752 for interval in self.entries: 753 if interval.start >= interval.end: 754 isValid = False 755 errorReporter( 756 errors.TextgridStateError, 757 f"Invalid interval. End time occurs before or on the start time({interval}).", 758 ) 759 760 if previousInterval and previousInterval.end > interval.start: 761 isValid = False 762 errorReporter( 763 errors.TextgridStateError, 764 f"Intervals are not sorted in time: " 765 f"[({previousInterval}), ({interval})]", 766 ) 767 768 if utils.checkIsUndershoot( 769 interval.start, self.minTimestamp, errorReporter 770 ): 771 isValid = False 772 773 if utils.checkIsOvershoot(interval.end, self.maxTimestamp, errorReporter): 774 isValid = False 775 776 previousInterval = interval 777 778 return isValid
Helper class that provides a standard way to create an ABC using inheritance.
62 def __init__( 63 self, 64 name: str, 65 entries: List[Interval], 66 minT: Optional[float] = None, 67 maxT: Optional[float] = None, 68 ): 69 """An interval tier is for annotating events that have duration 70 71 The entries is of the form: 72 [(startTime1, endTime1, label1), (startTime2, endTime2, label2), ] 73 74 The data stored in the labels can be anything but will 75 be interpreted as text by praatio (the label could be descriptive 76 text e.g. ('erase this region') or numerical data e.g. (average pitch 77 values like '132')) 78 """ 79 entries = _homogenizeEntries(entries) 80 calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT) 81 82 super(IntervalTier, self).__init__( 83 name, entries, calculatedMinT, calculatedMaxT 84 ) 85 self._validate()
An interval tier is for annotating events that have duration
The entries is of the form: [(startTime1, endTime1, label1), (startTime2, endTime2, label2), ]
The data stored in the labels can be anything but will be interpreted as text by praatio (the label could be descriptive text e.g. ('erase this region') or numerical data e.g. (average pitch values like '132'))
121 def crop( 122 self, 123 cropStart: float, 124 cropEnd: float, 125 mode: Literal["strict", "lax", "truncated"], 126 rebaseToZero: bool, 127 ) -> "IntervalTier": 128 """Creates a new tier with all entries that fit inside the new interval 129 130 Args: 131 cropStart: 132 cropEnd: 133 mode: determines cropping behavior 134 - 'strict', only intervals wholly contained by the crop 135 interval will be kept 136 - 'lax', partially contained intervals will be kept 137 - 'truncated', partially contained intervals will be 138 truncated to fit within the crop region. 139 rebaseToZero: if True, the cropped textgrid values 140 will be subtracted by the cropStart 141 142 Returns: 143 the modified version of the current tier 144 """ 145 146 utils.validateOption("mode", mode, CropCollision) 147 148 if cropStart >= cropEnd: 149 raise errors.ArgumentError( 150 f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})" 151 ) 152 153 newEntryList = utils.getIntervalsInInterval( 154 cropStart, cropEnd, self.entries, mode 155 ) 156 157 if rebaseToZero is True: 158 newSmallestValue = newEntryList[0][0] 159 if newSmallestValue < cropStart: 160 timeDiff = newSmallestValue 161 else: 162 timeDiff = cropStart 163 newEntryList = [ 164 Interval(start - timeDiff, end - timeDiff, label) 165 for start, end, label in newEntryList 166 ] 167 minT = 0.0 168 maxT = cropEnd - cropStart 169 else: 170 minT = cropStart 171 maxT = cropEnd 172 173 croppedTier = IntervalTier(self.name, newEntryList, minT, maxT) 174 175 return croppedTier
Creates a new tier with all entries that fit inside the new interval
Arguments:
- cropStart:
- cropEnd:
- mode: determines cropping 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 values will be subtracted by the cropStart
Returns:
the modified version of the current tier
177 def dejitter( 178 self, 179 referenceTier: textgrid_tier.TextgridTier, 180 maxDifference: float = 0.001, 181 ) -> textgrid_tier.TextgridTier: 182 """ 183 Set timestamps in this tier to be the same as values in the reference tier 184 185 Timestamps will only be moved if they are less than maxDifference away from the 186 reference time. 187 188 This can be used to correct minor alignment errors between tiers, as made when 189 annotating files manually, etc. 190 191 Args: 192 referenceTier: the IntervalTier or PointTier to use as a reference 193 maxDifference: the maximum amount to allow timestamps to be moved by 194 195 Returns: 196 the modified version of the current tier 197 """ 198 referenceTimestamps = referenceTier.timestamps 199 200 newEntries = [] 201 for start, stop, label in self.entries: 202 startCompare = min(referenceTimestamps, key=lambda x: abs(x - start)) 203 stopCompare = min(referenceTimestamps, key=lambda x: abs(x - stop)) 204 205 if my_math.lessThanOrEqual(abs(start - startCompare), maxDifference): 206 start = startCompare 207 if my_math.lessThanOrEqual(abs(stop - stopCompare), maxDifference): 208 stop = stopCompare 209 newEntries.append((start, stop, label)) 210 211 return self.new(entries=newEntries)
Set timestamps in this tier to be the same as values in the reference tier
Timestamps will only be moved if they are less than maxDifference away from the reference time.
This can be used to correct minor alignment errors between tiers, as made when annotating files manually, etc.
Arguments:
- referenceTier: the IntervalTier or PointTier to use as a reference
- maxDifference: the maximum amount to allow timestamps to be moved by
Returns:
the modified version of the current tier
213 def deleteEntry(self, entry: Interval) -> None: 214 """Removes an entry from the entries""" 215 self._entries.pop(self._entries.index(entry))
Removes an entry from the entries
217 def difference(self, tier: "IntervalTier") -> "IntervalTier": 218 """Takes the set difference of this tier and the given one 219 220 Any overlapping portions of entries with entries in this textgrid 221 will be removed from the returned tier. 222 223 Args: 224 tier: the tier to subtract from this one 225 226 Returns: 227 the modified version of the current tier 228 """ 229 retTier = self.new() 230 231 for entry in tier.entries: 232 retTier = retTier.eraseRegion( 233 entry.start, 234 entry.end, 235 collisionMode=constants.EraseCollision.TRUNCATE, 236 doShrink=False, 237 ) 238 239 return retTier
Takes the set difference of this tier and the given one
Any overlapping portions of entries with entries in this textgrid will be removed from the returned tier.
Arguments:
- tier: the tier to subtract from this one
Returns:
the modified version of the current tier
241 def editTimestamps( 242 self, 243 offset: float, 244 reportingMode: Literal["silence", "warning", "error"] = "warning", 245 ) -> "IntervalTier": 246 """Modifies all timestamps by a constant amount 247 248 Args: 249 offset: the amount to shift all intervals 250 reportingMode: Determines the behavior if an entries moves outside 251 of minTimestamp or maxTimestamp after being edited 252 253 Returns: 254 the modified version of the current tier 255 """ 256 utils.validateOption( 257 "reportingMode", reportingMode, constants.ErrorReportingMode 258 ) 259 errorReporter = utils.getErrorReporter(reportingMode) 260 261 newEntryList = [] 262 for interval in self.entries: 263 newStart = offset + interval.start 264 newEnd = offset + interval.end 265 266 utils.checkIsUndershoot(newStart, self.minTimestamp, errorReporter) 267 utils.checkIsOvershoot(newEnd, self.maxTimestamp, errorReporter) 268 269 if newEnd <= 0: 270 continue 271 if newStart < 0: 272 newStart = 0 273 274 newEntryList.append(Interval(newStart, newEnd, interval.label)) 275 276 # Determine new min and max timestamps 277 newMin = min([interval.start for interval in newEntryList]) 278 newMax = max([interval.end for interval in newEntryList]) 279 280 if newMin > self.minTimestamp: 281 newMin = self.minTimestamp 282 283 if newMax < self.maxTimestamp: 284 newMax = self.maxTimestamp 285 286 return IntervalTier(self.name, newEntryList, newMin, newMax)
Modifies all timestamps by a constant amount
Arguments:
- offset: the amount to shift all intervals
- reportingMode: Determines the behavior if an entries moves outside of minTimestamp or maxTimestamp after being edited
Returns:
the modified version of the current tier
288 def eraseRegion( 289 self, 290 start: float, 291 end: float, 292 collisionMode: Literal["truncate", "categorical", "error"] = "error", 293 doShrink: bool = True, 294 ) -> "IntervalTier": 295 """Makes a region in a tier blank (removes all contained entries) 296 297 Args: 298 start: 299 end: 300 collisionMode: Determines the behavior when the region to erase 301 overlaps with existing intervals. 302 - 'truncate' partially contained entries will have the portion 303 removed that overlaps with the target entry 304 - 'categorical' all entries that overlap, even partially, with 305 the target entry will be completely removed 306 - None or any other value throws IntervalCollision 307 doShrink: If True, moves leftward by (/end/ - /start/) 308 amount, each item that occurs after /end/ 309 310 Returns: 311 The modified version of the current tier 312 313 Raises: 314 CollisionError 315 """ 316 utils.validateOption("collisionMode", collisionMode, constants.EraseCollision) 317 318 matchList = self.crop(start, end, CropCollision.LAX, False).entries 319 newTier = self.new() 320 321 if len(matchList) == 0: 322 pass 323 else: 324 if collisionMode == constants.EraseCollision.ERROR: 325 raise errors.CollisionError( 326 f"Erase region ({start}, {end})overlapped with an interval. " 327 "If this was expected, consider setting the collisionMode" 328 ) 329 330 # Remove all the matches from the entries 331 # Go in reverse order because we're destructively altering 332 # the order of the list (messes up index order) 333 for interval in matchList[::-1]: 334 newTier.deleteEntry(interval) 335 336 # If we're only truncating, reinsert entries on the left and 337 # right edges 338 # if categorical, it doesn't make it into the list at all 339 if collisionMode == constants.EraseCollision.TRUNCATE: 340 # Check left edge 341 if matchList[0].start < start: 342 newEntry = Interval(matchList[0].start, start, matchList[0].label) 343 newTier.insertEntry(newEntry) 344 345 # Check right edge 346 if matchList[-1].end > end: 347 newEntry = Interval(end, matchList[-1].end, matchList[-1].label) 348 newTier.insertEntry(newEntry) 349 350 if doShrink is True: 351 diff = end - start 352 newEntryList = [] 353 for interval in newTier.entries: 354 if interval.end <= start: 355 newEntryList.append(interval) 356 elif interval.start >= end: 357 newEntryList.append( 358 Interval( 359 interval.start - diff, interval.end - diff, interval.label 360 ) 361 ) 362 363 # Special case: an interval that spanned the deleted 364 # section 365 for i in range(0, len(newEntryList) - 1): 366 rightEdge = newEntryList[i].end == start 367 leftEdge = newEntryList[i + 1].start == start 368 sameLabel = newEntryList[i].label == newEntryList[i + 1].label 369 if rightEdge and leftEdge and sameLabel: 370 newInterval = Interval( 371 newEntryList[i].start, 372 newEntryList[i + 1].end, 373 newEntryList[i].label, 374 ) 375 376 newEntryList.pop(i + 1) 377 newEntryList.pop(i) 378 newEntryList.insert(i, newInterval) 379 380 # Only one interval can span the deleted section, 381 # so if we've found it, move on 382 break 383 384 newMax = newTier.maxTimestamp - diff 385 newTier = newTier.new(entries=newEntryList, maxTimestamp=newMax) 386 387 return newTier
Makes a region in a tier blank (removes all contained entries)
Arguments:
- start:
- end:
- collisionMode: Determines the behavior when the region to erase
overlaps with existing intervals.
- 'truncate' partially contained entries will have the portion removed that overlaps with the target entry
- 'categorical' all entries that overlap, even partially, with the target entry will be completely removed
- None or any other value throws IntervalCollision
- doShrink: If True, moves leftward by (/end/ - /start/) amount, each item that occurs after /end/
Returns:
The modified version of the current tier
Raises:
- CollisionError
389 def getValuesInIntervals(self, dataTupleList: List) -> List[Tuple[Interval, List]]: 390 """Returns data from dataTupleList contained in labeled intervals 391 392 Each labeled interval will get its own list of data values. 393 394 dataTupleList should be of the form: 395 [(time1, value1a, value1b,...), (time2, value2a, value2b...), ...] 396 """ 397 398 returnList = [] 399 400 for interval in self.entries: 401 intervalDataList = utils.getValuesInInterval( 402 dataTupleList, interval.start, interval.end 403 ) 404 returnList.append((interval, intervalDataList)) 405 406 return returnList
Returns data from dataTupleList contained in labeled intervals
Each labeled interval will get its own list of data values.
dataTupleList should be of the form: [(time1, value1a, value1b,...), (time2, value2a, value2b...), ...]
408 def getNonEntries(self) -> List[Interval]: 409 """Returns the regions of the textgrid without labels 410 411 This can include unlabeled segments and regions marked as silent. 412 """ 413 entries = self.entries 414 invertedEntryList = [ 415 Interval(entries[i].end, entries[i + 1].start, "") 416 for i in range(len(entries) - 1) 417 ] 418 419 # Remove entries that have no duration (ie lie between two entries 420 # that share a border) 421 invertedEntryList = [ 422 interval for interval in invertedEntryList if interval.start < interval.end 423 ] 424 425 if entries[0].start > 0: 426 invertedEntryList.insert(0, Interval(0, entries[0].start, "")) 427 428 if entries[-1].end < self.maxTimestamp: 429 invertedEntryList.append(Interval(entries[-1].end, self.maxTimestamp, "")) 430 431 invertedEntryList = [ 432 interval if isinstance(interval, Interval) else Interval(*interval) 433 for interval in invertedEntryList 434 ] 435 436 return invertedEntryList
Returns the regions of the textgrid without labels
This can include unlabeled segments and regions marked as silent.
438 def insertEntry( 439 self, 440 entry: Interval, 441 collisionMode: Literal["replace", "merge", "error"] = "error", 442 collisionReportingMode: Literal["silence", "warning"] = "warning", 443 ) -> None: 444 """Inserts an interval into the tier 445 446 Args: 447 entry: the Interval to insert 448 collisionMode: determines the behavior in the event that intervals 449 exist in the insertion area. 450 - 'replace' will remove existing items 451 - 'merge' will fuse the inserting item with existing items 452 - None or any other value will throw a CollisionError 453 collisionReportingMode: Determines the behavior if the new entry 454 overlaps with an existing one 455 456 Returns: 457 the modified version of the current tier 458 """ 459 utils.validateOption( 460 "collisionMode", collisionMode, constants.IntervalCollision 461 ) 462 utils.validateOption( 463 "collisionReportingMode", 464 collisionReportingMode, 465 constants.ErrorReportingMode, 466 ) 467 collisionReporter = utils.getErrorReporter(collisionReportingMode) 468 469 if not isinstance(entry, Interval): 470 interval = Interval(*entry) 471 else: 472 interval = entry 473 474 matchList = self.crop( 475 interval.start, interval.end, CropCollision.LAX, False 476 )._entries 477 478 if len(matchList) == 0: 479 self._entries.append(interval) 480 481 elif collisionMode == constants.IntervalCollision.REPLACE: 482 for matchEntry in matchList: 483 self.deleteEntry(matchEntry) 484 self._entries.append(interval) 485 486 elif collisionMode == constants.IntervalCollision.MERGE: 487 for matchEntry in matchList: 488 self.deleteEntry(matchEntry) 489 matchList.append(interval) 490 matchList.sort() # By starting time 491 492 newInterval = Interval( 493 min([tmpInterval.start for tmpInterval in matchList]), 494 max([tmpInterval.end for tmpInterval in matchList]), 495 "-".join([tmpInterval.label for tmpInterval in matchList]), 496 ) 497 self._entries.append(newInterval) 498 499 else: 500 raise errors.CollisionError( 501 "Attempted to insert interval " 502 f"({interval.start}, {interval.end}, '{interval.label}') into tier {self.name} " 503 "of textgrid but overlapping entries " 504 f"{[tuple(interval) for interval in matchList]} " 505 "already exist" 506 ) 507 508 self.sort() 509 510 if self._entries[0][0] < self.minTimestamp: 511 self.minTimestamp = self._entries[0][0] 512 513 if self._entries[-1][1] > self.maxTimestamp: 514 self.maxTimestamp = self._entries[-1][1] 515 516 if len(matchList) != 0: 517 collisionReporter( 518 errors.CollisionError, 519 f"Collision warning for ({interval}) with items " 520 f"({matchList}) of tier '{self.name}'", 521 )
Inserts an interval into the tier
Arguments:
- entry: the Interval to insert
- collisionMode: determines the behavior in the event that intervals
exist in the insertion area.
- 'replace' will remove existing items
- 'merge' will fuse the inserting item with existing items
- None or any other value will throw a CollisionError
- collisionReportingMode: Determines the behavior if the new entry overlaps with an existing one
Returns:
the modified version of the current tier
523 def insertSpace( 524 self, 525 start: float, 526 duration: float, 527 collisionMode: Literal["stretch", "split", "no_change", "error"], 528 ) -> "IntervalTier": 529 """Inserts a blank region into the tier 530 531 Args: 532 start: 533 duration: 534 collisionMode: Determines the behavior that occurs if 535 an interval stradles the starting point 536 - 'stretch' stretches the interval by /duration/ amount 537 - 'split' splits the interval into two--everything to the 538 right of 'start' will be advanced by 'duration' seconds 539 - 'no change' leaves the interval as is with no change 540 - 'error' will stop execution and raise an error 541 542 Returns: 543 the modified version of the current tier 544 """ 545 utils.validateOption( 546 "collisionMode", collisionMode, constants.WhitespaceCollision 547 ) 548 549 newEntryList = [] 550 for interval in self.entries: 551 # Entry exists before the insertion point 552 if interval.end <= start: 553 newEntryList.append(interval) 554 # Entry exists after the insertion point 555 elif interval.start >= start: 556 newEntryList.append( 557 Interval( 558 interval.start + duration, 559 interval.end + duration, 560 interval.label, 561 ) 562 ) 563 # Entry straddles the insertion point 564 elif interval.start <= start and interval.end > start: 565 if collisionMode == constants.WhitespaceCollision.STRETCH: 566 newEntryList.append( 567 Interval( 568 interval.start, interval.end + duration, interval.label 569 ) 570 ) 571 elif collisionMode == constants.WhitespaceCollision.SPLIT: 572 # Left side of the split 573 newEntryList.append(Interval(interval.start, start, interval.label)) 574 # Right side of the split 575 newEntryList.append( 576 ( 577 start + duration, 578 start + duration + (interval.end - start), 579 interval.label, 580 ) 581 ) 582 elif collisionMode == constants.WhitespaceCollision.NO_CHANGE: 583 newEntryList.append(interval) 584 else: 585 raise errors.ArgumentError( 586 f"Collision occured during insertSpace() for interval '{interval}' " 587 f"and given white space insertion interval ({start}, {start + duration})" 588 ) 589 590 newTier = self.new( 591 entries=newEntryList, maxTimestamp=self.maxTimestamp + duration 592 ) 593 594 return newTier
Inserts a blank region into the tier
Arguments:
- start:
- duration:
- collisionMode: Determines the behavior that occurs if
an interval stradles the starting point
- '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
- 'error' will stop execution and raise an error
Returns:
the modified version of the current tier
596 def intersection(self, tier: "IntervalTier", demarcator="-") -> "IntervalTier": 597 """Takes the set intersection of this tier and the given one 598 599 - The output will contain one interval for each overlapping pair 600 e.g. [(1, 2, 'foo')] and [(1, 1.3, 'bang'), (1.7, 2, 'wizz')] 601 -> [(1, 1.3, 'foo-bang'), (1.7, 2, 'foo-wizz')] 602 - Only intervals that exist in both tiers will remain in the returned tier. 603 e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] 604 -> [(1, 2, 'foo-bang')] 605 - If intervals partially overlap, only the overlapping portion will be returned. 606 e.g. [(1, 2, 'foo')] and [(0.5, 1.5, 'bang')] 607 -> [(1, 1.5, 'foo-bang')] 608 609 Compare with IntervalTier.mergeLabels 610 611 Args: 612 tier: the tier to intersect with 613 demarcator: the character to separate the labels of the overlapping intervals 614 615 Returns: 616 IntervalTier: the modified version of the current tier 617 """ 618 retEntryList = [] 619 for interval in tier.entries: 620 subTier = self.crop( 621 interval.start, interval.end, CropCollision.TRUNCATED, False 622 ) 623 624 # Combine the labels in the two tiers 625 subEntryList = [ 626 ( 627 subInterval.start, 628 subInterval.end, 629 f"{subInterval.label}{demarcator}{interval.label}", 630 ) 631 for subInterval in subTier.entries 632 ] 633 634 retEntryList.extend(subEntryList) 635 636 newName = f"{self.name}-{tier.name}" 637 638 retTier = self.new(newName, retEntryList) 639 640 return retTier
Takes the set intersection of this tier and the given one
- The output will contain one interval for each overlapping pair e.g. [(1, 2, 'foo')] and [(1, 1.3, 'bang'), (1.7, 2, 'wizz')] -> [(1, 1.3, 'foo-bang'), (1.7, 2, 'foo-wizz')]
- Only intervals that exist in both tiers will remain in the returned tier. e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] -> [(1, 2, 'foo-bang')]
- If intervals partially overlap, only the overlapping portion will be returned. e.g. [(1, 2, 'foo')] and [(0.5, 1.5, 'bang')] -> [(1, 1.5, 'foo-bang')]
Compare with IntervalTier.mergeLabels
Arguments:
- tier: the tier to intersect with
- demarcator: the character to separate the labels of the overlapping intervals
Returns:
IntervalTier: the modified version of the current tier
642 def mergeLabels( 643 self, tier: "IntervalTier", demarcator: str = "," 644 ) -> "IntervalTier": 645 """Merges labels of overlapping tiers into this tier 646 647 - All intervals in this tier will appear in the output; for the given tier, only intervals 648 that overlap with content in this tier will appear in the output 649 e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] 650 -> [(1, 2, 'foo(bang)'), (3, 4, 'bar()')] 651 - If multiple entries exist in a subinterval, their labels will be concatenated 652 e.g. [(1, 2, 'hi')] and [(1, 1.5, 'h'), (1.5, 2, 'ai')] -> [(1, 2, 'hi(h,ai)')] 653 654 compare with IntervalTier.intersection 655 656 Args: 657 tier: the tier to intersect with 658 demarcator: the string to separate items that fall in the same subinterval 659 660 Returns: 661 IntervalTier: the modified version of the current tier 662 """ 663 retEntryList = [] 664 for interval in self.entries: 665 subTier = tier.crop( 666 interval.start, interval.end, CropCollision.TRUNCATED, False 667 ) 668 if len(subTier._entries) == 0: 669 continue 670 671 subLabel = demarcator.join([entry.label for entry in subTier.entries]) 672 label = f"{interval.label}({subLabel})" 673 674 start = min(interval.start, subTier._entries[0].start) 675 end = max(interval.end, subTier._entries[-1].end) 676 677 intersectedInterval = ( 678 start, 679 end, 680 label, 681 ) 682 683 retEntryList.append(intersectedInterval) 684 685 newName = f"{self.name}-{tier.name}" 686 687 retTier = self.new(newName, retEntryList) 688 689 return retTier
Merges labels of overlapping tiers into this tier
- All intervals in this tier will appear in the output; for the given tier, only intervals that overlap with content in this tier will appear in the output e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] -> [(1, 2, 'foo(bang)'), (3, 4, 'bar()')]
- If multiple entries exist in a subinterval, their labels will be concatenated e.g. [(1, 2, 'hi')] and [(1, 1.5, 'h'), (1.5, 2, 'ai')] -> [(1, 2, 'hi(h,ai)')]
compare with IntervalTier.intersection
Arguments:
- tier: the tier to intersect with
- demarcator: the string to separate items that fall in the same subinterval
Returns:
IntervalTier: the modified version of the current tier
691 def morph( 692 self, 693 targetTier: "IntervalTier", 694 filterFunc: Optional[Callable[[str], bool]] = None, 695 ) -> "IntervalTier": 696 """Morphs the duration of segments in this tier to those in another 697 698 This preserves the labels and the duration of silence in 699 this tier while changing the duration of labeled segments. 700 701 Args: 702 targetTier: 703 filterFunc: if specified, filters entries. The 704 functor takes one argument, an Interval. It returns true 705 if the Interval should be modified and false if not. 706 707 Returns: 708 The modified version of the current tier 709 """ 710 cumulativeAdjustAmount = 0 711 newEntryList = [] 712 allIntervals = [self.entries, targetTier.entries] 713 for sourceInterval, targetInterval in utils.safeZip(allIntervals, True): 714 # sourceInterval.start - lastFromEnd -> was this interval and the 715 # last one adjacent? 716 newStart = sourceInterval.start + cumulativeAdjustAmount 717 718 currIntervalDuration = sourceInterval.end - sourceInterval.start 719 if filterFunc is None or filterFunc(sourceInterval.label): 720 newIntervalDuration = targetInterval.end - targetInterval.start 721 cumulativeAdjustAmount += newIntervalDuration - currIntervalDuration 722 newEnd = newStart + newIntervalDuration 723 else: 724 newEnd = newStart + currIntervalDuration 725 726 newEntryList.append(Interval(newStart, newEnd, sourceInterval.label)) 727 728 newMin = self.minTimestamp 729 cumulativeDifference = newEntryList[-1].end - self.entries[-1].end 730 newMax = self.maxTimestamp + cumulativeDifference 731 732 return IntervalTier(self.name, newEntryList, newMin, newMax)
Morphs the duration of segments in this tier to those in another
This preserves the labels and the duration of silence in this tier while changing the duration of labeled segments.
Arguments:
- targetTier:
- filterFunc: if specified, filters entries. The functor takes one argument, an Interval. It returns true if the Interval should be modified and false if not.
Returns:
The modified version of the current tier
734 def validate( 735 self, reportingMode: Literal["silence", "warning", "error"] = "warning" 736 ) -> bool: 737 """Validate this tier 738 739 Args: 740 reportingMode (str): Determines the behavior if validation fails. 741 742 Returns: 743 True if the tier is valid; False if not 744 """ 745 utils.validateOption( 746 "reportingMode", reportingMode, constants.ErrorReportingMode 747 ) 748 errorReporter = utils.getErrorReporter(reportingMode) 749 750 isValid = True 751 previousInterval = None 752 for interval in self.entries: 753 if interval.start >= interval.end: 754 isValid = False 755 errorReporter( 756 errors.TextgridStateError, 757 f"Invalid interval. End time occurs before or on the start time({interval}).", 758 ) 759 760 if previousInterval and previousInterval.end > interval.start: 761 isValid = False 762 errorReporter( 763 errors.TextgridStateError, 764 f"Intervals are not sorted in time: " 765 f"[({previousInterval}), ({interval})]", 766 ) 767 768 if utils.checkIsUndershoot( 769 interval.start, self.minTimestamp, errorReporter 770 ): 771 isValid = False 772 773 if utils.checkIsOvershoot(interval.end, self.maxTimestamp, errorReporter): 774 isValid = False 775 776 previousInterval = interval 777 778 return isValid
Validate this tier
Arguments:
- reportingMode (str): Determines the behavior if validation fails.
Returns:
True if the tier is valid; False if not