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}
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
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
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
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
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
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
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
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
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
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
443 def new(self) -> "Textgrid": 444 """Returns a copy of this Textgrid""" 445 return copy.deepcopy(self)
Returns a copy of this Textgrid
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
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