praatio.data_classes.point_tier
A PointTier is a tier containing an array of points -- data that exists at a specific point in time
1""" 2A PointTier is a tier containing an array of points -- data that exists at a specific point in time 3""" 4from typing import List, Tuple, Optional, Any, Sequence 5 6from typing_extensions import Literal 7 8from praatio.utilities.constants import ( 9 Point, 10 POINT_TIER, 11) 12from praatio.utilities import constants 13from praatio.utilities import errors 14from praatio.utilities import utils 15from praatio.utilities import my_math 16 17from praatio.data_classes import textgrid_tier 18 19 20def _homogenizeEntries(entries): 21 """ 22 Enforces consistency in points 23 24 - converts all entries to points 25 - removes whitespace in labels 26 - sorts values by time 27 """ 28 processedEntries = [Point(float(time), label.strip()) for time, label in entries] 29 processedEntries.sort() 30 return processedEntries 31 32 33def _calculateMinAndMaxTime(entries: Sequence[Point], minT=None, maxT=None): 34 timeList = [time for time, label in entries] 35 if minT is not None: 36 timeList.append(float(minT)) 37 if maxT is not None: 38 timeList.append(float(maxT)) 39 40 try: 41 calculatedMinT = min(timeList) 42 calculatedMaxT = max(timeList) 43 except ValueError: 44 raise errors.TimelessTextgridTierException() 45 46 return (calculatedMinT, calculatedMaxT) 47 48 49class PointTier(textgrid_tier.TextgridTier): 50 tierType = POINT_TIER 51 entryType = Point 52 53 def __init__( 54 self, 55 name: str, 56 entries: List[Point], 57 minT: Optional[float] = None, 58 maxT: Optional[float] = None, 59 ): 60 """A point tier is for annotating instaneous events 61 62 The entries is of the form: 63 [(timeVal1, label1), (timeVal2, label2), ] 64 65 The data stored in the labels can be anything but will 66 be interpreted as text by praatio (the label could be descriptive 67 text e.g. ('peak point here') or numerical data e.g. (pitch values 68 like '132')) 69 """ 70 entries = _homogenizeEntries(entries) 71 calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT) 72 73 super(PointTier, self).__init__(name, entries, calculatedMinT, calculatedMaxT) 74 75 @property 76 def timestamps(self) -> List[float]: 77 """All unique timestamps used in this tier""" 78 tmpTimestamps = [time for time, _ in self.entries] 79 80 uniqueTimestamps = list(set(tmpTimestamps)) 81 uniqueTimestamps.sort() 82 83 return uniqueTimestamps 84 85 def crop( 86 self, 87 cropStart: float, 88 cropEnd: float, 89 mode: Literal["strict", "lax", "truncated"] = "lax", 90 rebaseToZero: bool = True, 91 ) -> "PointTier": 92 """Creates a new tier containing all entries inside the new interval 93 94 Args: 95 cropStart: 96 cropEnd: 97 mode: Mode is ignored. This parameter is kept for 98 compatibility with IntervalTier.crop() 99 rebaseToZero: if True, all entries will have their 100 timestamps subtracted by *cropStart*. 101 102 Returns: 103 the modified version of the current tier 104 """ 105 if cropStart >= cropEnd: 106 raise errors.ArgumentError( 107 f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})" 108 ) 109 110 newEntries = [] 111 112 for entry in self.entries: 113 timestamp = entry.time 114 115 if timestamp >= cropStart and timestamp <= cropEnd: 116 newEntries.append(entry) 117 118 if rebaseToZero is True: 119 newEntries = [ 120 Point(timeV - cropStart, label) for timeV, label in newEntries 121 ] 122 minT = 0.0 123 maxT = cropEnd - cropStart 124 else: 125 minT = cropStart 126 maxT = cropEnd 127 128 return PointTier(self.name, newEntries, minT, maxT) 129 130 def deleteEntry(self, entry: Point) -> None: 131 """Removes an entry from the entries""" 132 self._entries.pop(self._entries.index(entry)) 133 134 def dejitter( 135 self, referenceTier: textgrid_tier.TextgridTier, maxDifference: float = 0.001 136 ) -> "PointTier": 137 """ 138 Set timestamps in this tier to be the same as values in the reference tier 139 140 Timestamps will only be moved if they are less than maxDifference away from the 141 reference time. 142 143 This can be used to correct minor alignment errors between tiers, as made when 144 annotating files manually, etc. 145 146 Args: 147 referenceTier: the IntervalTier or PointTier to use as a reference 148 maxDifference: the maximum amount to allow timestamps to be moved by 149 150 Returns: 151 the modified version of the current tier 152 """ 153 referenceTimestamps = referenceTier.timestamps 154 155 newEntries = [] 156 for time, label in self.entries: 157 timeCompare = min(referenceTimestamps, key=lambda x: abs(x - time)) 158 159 if my_math.lessThanOrEqual(abs(time - timeCompare), maxDifference): 160 time = timeCompare 161 newEntries.append((time, label)) 162 163 return self.new(entries=newEntries) 164 165 def editTimestamps( 166 self, 167 offset: float, 168 reportingMode: Literal["silence", "warning", "error"] = "warning", 169 ) -> "PointTier": 170 """Modifies all timestamps by a constant amount 171 172 Args: 173 offset: 174 reportingMode: one of "silence", "warning", or "error". This flag 175 determines the behavior if an entries moves outside of minTimestamp 176 or maxTimestamp after being edited 177 178 Returns: 179 the modified version of the current tier 180 """ 181 utils.validateOption( 182 "reportingMode", reportingMode, constants.ErrorReportingMode 183 ) 184 errorReporter = utils.getErrorReporter(reportingMode) 185 186 newEntries: List[Point] = [] 187 for timestamp, label in self.entries: 188 newTimestamp = timestamp + offset 189 utils.checkIsUndershoot(newTimestamp, self.minTimestamp, errorReporter) 190 utils.checkIsOvershoot(newTimestamp, self.maxTimestamp, errorReporter) 191 192 if newTimestamp < 0: 193 continue 194 195 newEntries.append(Point(newTimestamp, label)) 196 197 # Determine new min and max timestamps 198 timeList = [float(point.time) for point in newEntries] 199 newMin = min(timeList) 200 newMax = max(timeList) 201 202 if newMin > self.minTimestamp: 203 newMin = self.minTimestamp 204 205 if newMax < self.maxTimestamp: 206 newMax = self.maxTimestamp 207 208 return PointTier(self.name, newEntries, newMin, newMax) 209 210 def getValuesAtPoints( 211 self, 212 dataTupleList: List[Tuple[float, ...]], 213 fuzzyMatching: bool = False, 214 ) -> List[Tuple[Any, ...]]: 215 """Get the values that occur at points in the point tier 216 217 The procedure assumes that all data is ordered in time. 218 dataTupleList should be in the form 219 [(t1, v1a, v1b, ..), (t2, v2a, v2b, ..), ..] 220 221 It returns the data in the form of 222 [(t1, v1a, v1b, ..), (t2, v2a, v2b), ..] 223 224 The procedure makes one pass through dataTupleList and one 225 pass through self.entries. If the data is not sequentially 226 ordered, the incorrect response will be returned. 227 228 Args: 229 dataTupleList: 230 fuzzyMatching: if True, if there is not a feature value 231 at a point, the nearest feature value will be taken. 232 233 Returns: 234 A list of values that exist at the given timepoints 235 """ 236 237 currentIndex = 0 238 retList = [] 239 240 sortedDataTupleList = sorted(dataTupleList) 241 for timestamp, label in self.entries: 242 retTuple = utils.getValueAtTime( 243 timestamp, 244 sortedDataTupleList, 245 fuzzyMatching=fuzzyMatching, 246 startI=currentIndex, 247 ) 248 retRow, currentIndex = retTuple 249 retList.append(retRow) 250 251 return retList 252 253 def eraseRegion( 254 self, 255 start: float, 256 end: float, 257 collisionMode: Literal["truncate", "categorical", "error"] = "error", 258 doShrink: bool = True, 259 ) -> "PointTier": 260 """Makes a region in a tier blank (removes all contained entries) 261 262 Args: 263 start: the start of the deletion interval 264 end: the end of the deletion interval 265 collisionMode: Ignored for the moment (added for compatibility with 266 eraseRegion() for Interval Tiers) 267 doShrink: if True, moves leftward by (/end/ - /start/) all points 268 to the right of /end/ 269 270 Returns: 271 The modified version of the current tier 272 """ 273 274 newTier = self.new() 275 croppedTier = newTier.crop(start, end, constants.CropCollision.TRUNCATED, False) 276 matchList = croppedTier.entries 277 278 if len(matchList) > 0: 279 # Remove all the matches from the entries 280 # Go in reverse order because we're destructively altering 281 # the order of the list (messes up index order) 282 for point in matchList[::-1]: 283 newTier.deleteEntry(point) 284 285 if doShrink is True: 286 newEntries = [] 287 diff = end - start 288 for point in newTier.entries: 289 if point.time < start: 290 newEntries.append(point) 291 elif point.time > end: 292 newEntries.append(Point(point.time - diff, point.label)) 293 294 newMax = newTier.maxTimestamp - diff 295 newTier = newTier.new(entries=newEntries, maxTimestamp=newMax) 296 297 return newTier 298 299 def insertEntry( 300 self, 301 entry: Point, 302 collisionMode: Literal["replace", "merge", "error"] = "error", 303 collisionReportingMode: Literal["silence", "warning"] = "warning", 304 ) -> None: 305 """Inserts an interval into the tier 306 307 Args: 308 entry: the entry to insert 309 collisionMode: determines the behavior if intervals exist in 310 the insertion area. 311 - 'replace', existing items will be removed 312 - 'merge', inserting item will be fused with existing items 313 - 'error', will throw TextgridCollisionException 314 collisionReportingMode: 315 316 Returns: 317 None 318 """ 319 320 utils.validateOption( 321 "collisionMode", collisionMode, constants.IntervalCollision 322 ) 323 utils.validateOption( 324 "collisionReportingMode", 325 collisionReportingMode, 326 constants.ErrorReportingMode, 327 ) 328 collisionReporter = utils.getErrorReporter(collisionReportingMode) 329 330 if not isinstance(entry, Point): 331 newPoint = Point(entry[0], entry[1]) 332 else: 333 newPoint = entry 334 335 matchList = [] 336 i = None 337 for i, point in enumerate(self.entries): 338 if point.time == newPoint.time: 339 matchList.append(point) 340 break 341 342 if len(matchList) == 0: 343 self._entries.append(newPoint) 344 345 elif collisionMode == constants.IntervalCollision.REPLACE: 346 self.deleteEntry(self.entries[i]) 347 self._entries.append(newPoint) 348 349 elif collisionMode == constants.IntervalCollision.MERGE: 350 oldPoint = self.entries[i] 351 mergedPoint = Point( 352 newPoint.time, "-".join([oldPoint.label, newPoint.label]) 353 ) 354 self.deleteEntry(self._entries[i]) 355 self._entries.append(mergedPoint) 356 357 else: 358 raise errors.CollisionError( 359 f"Attempted to insert interval {point} into tier {self.name} " 360 "of textgrid but overlapping entries " 361 f"{[tuple(interval) for interval in matchList]} " 362 "already exist" 363 ) 364 365 self.sort() 366 367 if len(matchList) != 0: 368 collisionReporter( 369 errors.CollisionError, 370 f"Collision warning for ({point}) with items ({matchList}) of tier '{self.name}'", 371 ) 372 373 def insertSpace( 374 self, 375 start: float, 376 duration: float, 377 _collisionMode: Literal["stretch", "split", "no_change", "error"] = "error", 378 ) -> "PointTier": 379 """Inserts a region into the tier 380 381 Args: 382 start: the start time to insert a space at 383 duration: the duration of the space to insert 384 collisionMode: Ignored for the moment (added for compatibility 385 with insertSpace() for Interval Tiers) 386 387 Returns: 388 PointTier: the modified version of the current tier 389 """ 390 391 newEntries = [] 392 for point in self.entries: 393 if point.time <= start: 394 newEntries.append(point) 395 elif point.time > start: 396 newEntries.append(Point(point.time + duration, point.label)) 397 398 newTier = self.new( 399 entries=newEntries, maxTimestamp=self.maxTimestamp + duration 400 ) 401 402 return newTier 403 404 def validate( 405 self, reportingMode: Literal["silence", "warning", "error"] = "warning" 406 ) -> bool: 407 """Validate this tier 408 409 Returns whether the tier is valid or not. If reportingMode is "warning" 410 or "error" this will also print on error or stop execution, respectively. 411 412 Args: 413 reportingMode: Determines the behavior if there is a size difference 414 between the maxTimestamp in the tier and the current textgrid. 415 416 Returns: 417 True if this tier is valid; False otherwise 418 """ 419 utils.validateOption( 420 "reportingMode", reportingMode, constants.ErrorReportingMode 421 ) 422 errorReporter = utils.getErrorReporter(reportingMode) 423 424 isValid = True 425 previousPoint = None 426 for point in self.entries: 427 if previousPoint and previousPoint.time > point.time: 428 isValid = False 429 errorReporter( 430 errors.TextgridStateError, 431 f"Points are not sorted in time: " 432 f"[({previousPoint}), ({point})]", 433 ) 434 435 if utils.checkIsUndershoot(point.time, self.minTimestamp, errorReporter): 436 isValid = False 437 438 if utils.checkIsOvershoot(point.time, self.maxTimestamp, errorReporter): 439 isValid = False 440 441 previousPoint = point 442 443 return isValid
50class PointTier(textgrid_tier.TextgridTier): 51 tierType = POINT_TIER 52 entryType = Point 53 54 def __init__( 55 self, 56 name: str, 57 entries: List[Point], 58 minT: Optional[float] = None, 59 maxT: Optional[float] = None, 60 ): 61 """A point tier is for annotating instaneous events 62 63 The entries is of the form: 64 [(timeVal1, label1), (timeVal2, label2), ] 65 66 The data stored in the labels can be anything but will 67 be interpreted as text by praatio (the label could be descriptive 68 text e.g. ('peak point here') or numerical data e.g. (pitch values 69 like '132')) 70 """ 71 entries = _homogenizeEntries(entries) 72 calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT) 73 74 super(PointTier, self).__init__(name, entries, calculatedMinT, calculatedMaxT) 75 76 @property 77 def timestamps(self) -> List[float]: 78 """All unique timestamps used in this tier""" 79 tmpTimestamps = [time for time, _ in self.entries] 80 81 uniqueTimestamps = list(set(tmpTimestamps)) 82 uniqueTimestamps.sort() 83 84 return uniqueTimestamps 85 86 def crop( 87 self, 88 cropStart: float, 89 cropEnd: float, 90 mode: Literal["strict", "lax", "truncated"] = "lax", 91 rebaseToZero: bool = True, 92 ) -> "PointTier": 93 """Creates a new tier containing all entries inside the new interval 94 95 Args: 96 cropStart: 97 cropEnd: 98 mode: Mode is ignored. This parameter is kept for 99 compatibility with IntervalTier.crop() 100 rebaseToZero: if True, all entries will have their 101 timestamps subtracted by *cropStart*. 102 103 Returns: 104 the modified version of the current tier 105 """ 106 if cropStart >= cropEnd: 107 raise errors.ArgumentError( 108 f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})" 109 ) 110 111 newEntries = [] 112 113 for entry in self.entries: 114 timestamp = entry.time 115 116 if timestamp >= cropStart and timestamp <= cropEnd: 117 newEntries.append(entry) 118 119 if rebaseToZero is True: 120 newEntries = [ 121 Point(timeV - cropStart, label) for timeV, label in newEntries 122 ] 123 minT = 0.0 124 maxT = cropEnd - cropStart 125 else: 126 minT = cropStart 127 maxT = cropEnd 128 129 return PointTier(self.name, newEntries, minT, maxT) 130 131 def deleteEntry(self, entry: Point) -> None: 132 """Removes an entry from the entries""" 133 self._entries.pop(self._entries.index(entry)) 134 135 def dejitter( 136 self, referenceTier: textgrid_tier.TextgridTier, maxDifference: float = 0.001 137 ) -> "PointTier": 138 """ 139 Set timestamps in this tier to be the same as values in the reference tier 140 141 Timestamps will only be moved if they are less than maxDifference away from the 142 reference time. 143 144 This can be used to correct minor alignment errors between tiers, as made when 145 annotating files manually, etc. 146 147 Args: 148 referenceTier: the IntervalTier or PointTier to use as a reference 149 maxDifference: the maximum amount to allow timestamps to be moved by 150 151 Returns: 152 the modified version of the current tier 153 """ 154 referenceTimestamps = referenceTier.timestamps 155 156 newEntries = [] 157 for time, label in self.entries: 158 timeCompare = min(referenceTimestamps, key=lambda x: abs(x - time)) 159 160 if my_math.lessThanOrEqual(abs(time - timeCompare), maxDifference): 161 time = timeCompare 162 newEntries.append((time, label)) 163 164 return self.new(entries=newEntries) 165 166 def editTimestamps( 167 self, 168 offset: float, 169 reportingMode: Literal["silence", "warning", "error"] = "warning", 170 ) -> "PointTier": 171 """Modifies all timestamps by a constant amount 172 173 Args: 174 offset: 175 reportingMode: one of "silence", "warning", or "error". This flag 176 determines the behavior if an entries moves outside of minTimestamp 177 or maxTimestamp after being edited 178 179 Returns: 180 the modified version of the current tier 181 """ 182 utils.validateOption( 183 "reportingMode", reportingMode, constants.ErrorReportingMode 184 ) 185 errorReporter = utils.getErrorReporter(reportingMode) 186 187 newEntries: List[Point] = [] 188 for timestamp, label in self.entries: 189 newTimestamp = timestamp + offset 190 utils.checkIsUndershoot(newTimestamp, self.minTimestamp, errorReporter) 191 utils.checkIsOvershoot(newTimestamp, self.maxTimestamp, errorReporter) 192 193 if newTimestamp < 0: 194 continue 195 196 newEntries.append(Point(newTimestamp, label)) 197 198 # Determine new min and max timestamps 199 timeList = [float(point.time) for point in newEntries] 200 newMin = min(timeList) 201 newMax = max(timeList) 202 203 if newMin > self.minTimestamp: 204 newMin = self.minTimestamp 205 206 if newMax < self.maxTimestamp: 207 newMax = self.maxTimestamp 208 209 return PointTier(self.name, newEntries, newMin, newMax) 210 211 def getValuesAtPoints( 212 self, 213 dataTupleList: List[Tuple[float, ...]], 214 fuzzyMatching: bool = False, 215 ) -> List[Tuple[Any, ...]]: 216 """Get the values that occur at points in the point tier 217 218 The procedure assumes that all data is ordered in time. 219 dataTupleList should be in the form 220 [(t1, v1a, v1b, ..), (t2, v2a, v2b, ..), ..] 221 222 It returns the data in the form of 223 [(t1, v1a, v1b, ..), (t2, v2a, v2b), ..] 224 225 The procedure makes one pass through dataTupleList and one 226 pass through self.entries. If the data is not sequentially 227 ordered, the incorrect response will be returned. 228 229 Args: 230 dataTupleList: 231 fuzzyMatching: if True, if there is not a feature value 232 at a point, the nearest feature value will be taken. 233 234 Returns: 235 A list of values that exist at the given timepoints 236 """ 237 238 currentIndex = 0 239 retList = [] 240 241 sortedDataTupleList = sorted(dataTupleList) 242 for timestamp, label in self.entries: 243 retTuple = utils.getValueAtTime( 244 timestamp, 245 sortedDataTupleList, 246 fuzzyMatching=fuzzyMatching, 247 startI=currentIndex, 248 ) 249 retRow, currentIndex = retTuple 250 retList.append(retRow) 251 252 return retList 253 254 def eraseRegion( 255 self, 256 start: float, 257 end: float, 258 collisionMode: Literal["truncate", "categorical", "error"] = "error", 259 doShrink: bool = True, 260 ) -> "PointTier": 261 """Makes a region in a tier blank (removes all contained entries) 262 263 Args: 264 start: the start of the deletion interval 265 end: the end of the deletion interval 266 collisionMode: Ignored for the moment (added for compatibility with 267 eraseRegion() for Interval Tiers) 268 doShrink: if True, moves leftward by (/end/ - /start/) all points 269 to the right of /end/ 270 271 Returns: 272 The modified version of the current tier 273 """ 274 275 newTier = self.new() 276 croppedTier = newTier.crop(start, end, constants.CropCollision.TRUNCATED, False) 277 matchList = croppedTier.entries 278 279 if len(matchList) > 0: 280 # Remove all the matches from the entries 281 # Go in reverse order because we're destructively altering 282 # the order of the list (messes up index order) 283 for point in matchList[::-1]: 284 newTier.deleteEntry(point) 285 286 if doShrink is True: 287 newEntries = [] 288 diff = end - start 289 for point in newTier.entries: 290 if point.time < start: 291 newEntries.append(point) 292 elif point.time > end: 293 newEntries.append(Point(point.time - diff, point.label)) 294 295 newMax = newTier.maxTimestamp - diff 296 newTier = newTier.new(entries=newEntries, maxTimestamp=newMax) 297 298 return newTier 299 300 def insertEntry( 301 self, 302 entry: Point, 303 collisionMode: Literal["replace", "merge", "error"] = "error", 304 collisionReportingMode: Literal["silence", "warning"] = "warning", 305 ) -> None: 306 """Inserts an interval into the tier 307 308 Args: 309 entry: the entry to insert 310 collisionMode: determines the behavior if intervals exist in 311 the insertion area. 312 - 'replace', existing items will be removed 313 - 'merge', inserting item will be fused with existing items 314 - 'error', will throw TextgridCollisionException 315 collisionReportingMode: 316 317 Returns: 318 None 319 """ 320 321 utils.validateOption( 322 "collisionMode", collisionMode, constants.IntervalCollision 323 ) 324 utils.validateOption( 325 "collisionReportingMode", 326 collisionReportingMode, 327 constants.ErrorReportingMode, 328 ) 329 collisionReporter = utils.getErrorReporter(collisionReportingMode) 330 331 if not isinstance(entry, Point): 332 newPoint = Point(entry[0], entry[1]) 333 else: 334 newPoint = entry 335 336 matchList = [] 337 i = None 338 for i, point in enumerate(self.entries): 339 if point.time == newPoint.time: 340 matchList.append(point) 341 break 342 343 if len(matchList) == 0: 344 self._entries.append(newPoint) 345 346 elif collisionMode == constants.IntervalCollision.REPLACE: 347 self.deleteEntry(self.entries[i]) 348 self._entries.append(newPoint) 349 350 elif collisionMode == constants.IntervalCollision.MERGE: 351 oldPoint = self.entries[i] 352 mergedPoint = Point( 353 newPoint.time, "-".join([oldPoint.label, newPoint.label]) 354 ) 355 self.deleteEntry(self._entries[i]) 356 self._entries.append(mergedPoint) 357 358 else: 359 raise errors.CollisionError( 360 f"Attempted to insert interval {point} into tier {self.name} " 361 "of textgrid but overlapping entries " 362 f"{[tuple(interval) for interval in matchList]} " 363 "already exist" 364 ) 365 366 self.sort() 367 368 if len(matchList) != 0: 369 collisionReporter( 370 errors.CollisionError, 371 f"Collision warning for ({point}) with items ({matchList}) of tier '{self.name}'", 372 ) 373 374 def insertSpace( 375 self, 376 start: float, 377 duration: float, 378 _collisionMode: Literal["stretch", "split", "no_change", "error"] = "error", 379 ) -> "PointTier": 380 """Inserts a region into the tier 381 382 Args: 383 start: the start time to insert a space at 384 duration: the duration of the space to insert 385 collisionMode: Ignored for the moment (added for compatibility 386 with insertSpace() for Interval Tiers) 387 388 Returns: 389 PointTier: the modified version of the current tier 390 """ 391 392 newEntries = [] 393 for point in self.entries: 394 if point.time <= start: 395 newEntries.append(point) 396 elif point.time > start: 397 newEntries.append(Point(point.time + duration, point.label)) 398 399 newTier = self.new( 400 entries=newEntries, maxTimestamp=self.maxTimestamp + duration 401 ) 402 403 return newTier 404 405 def validate( 406 self, reportingMode: Literal["silence", "warning", "error"] = "warning" 407 ) -> bool: 408 """Validate this tier 409 410 Returns whether the tier is valid or not. If reportingMode is "warning" 411 or "error" this will also print on error or stop execution, respectively. 412 413 Args: 414 reportingMode: Determines the behavior if there is a size difference 415 between the maxTimestamp in the tier and the current textgrid. 416 417 Returns: 418 True if this tier is valid; False otherwise 419 """ 420 utils.validateOption( 421 "reportingMode", reportingMode, constants.ErrorReportingMode 422 ) 423 errorReporter = utils.getErrorReporter(reportingMode) 424 425 isValid = True 426 previousPoint = None 427 for point in self.entries: 428 if previousPoint and previousPoint.time > point.time: 429 isValid = False 430 errorReporter( 431 errors.TextgridStateError, 432 f"Points are not sorted in time: " 433 f"[({previousPoint}), ({point})]", 434 ) 435 436 if utils.checkIsUndershoot(point.time, self.minTimestamp, errorReporter): 437 isValid = False 438 439 if utils.checkIsOvershoot(point.time, self.maxTimestamp, errorReporter): 440 isValid = False 441 442 previousPoint = point 443 444 return isValid
Helper class that provides a standard way to create an ABC using inheritance.
54 def __init__( 55 self, 56 name: str, 57 entries: List[Point], 58 minT: Optional[float] = None, 59 maxT: Optional[float] = None, 60 ): 61 """A point tier is for annotating instaneous events 62 63 The entries is of the form: 64 [(timeVal1, label1), (timeVal2, label2), ] 65 66 The data stored in the labels can be anything but will 67 be interpreted as text by praatio (the label could be descriptive 68 text e.g. ('peak point here') or numerical data e.g. (pitch values 69 like '132')) 70 """ 71 entries = _homogenizeEntries(entries) 72 calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT) 73 74 super(PointTier, self).__init__(name, entries, calculatedMinT, calculatedMaxT)
A point tier is for annotating instaneous events
The entries is of the form: [(timeVal1, label1), (timeVal2, 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. ('peak point here') or numerical data e.g. (pitch values like '132'))
86 def crop( 87 self, 88 cropStart: float, 89 cropEnd: float, 90 mode: Literal["strict", "lax", "truncated"] = "lax", 91 rebaseToZero: bool = True, 92 ) -> "PointTier": 93 """Creates a new tier containing all entries inside the new interval 94 95 Args: 96 cropStart: 97 cropEnd: 98 mode: Mode is ignored. This parameter is kept for 99 compatibility with IntervalTier.crop() 100 rebaseToZero: if True, all entries will have their 101 timestamps subtracted by *cropStart*. 102 103 Returns: 104 the modified version of the current tier 105 """ 106 if cropStart >= cropEnd: 107 raise errors.ArgumentError( 108 f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})" 109 ) 110 111 newEntries = [] 112 113 for entry in self.entries: 114 timestamp = entry.time 115 116 if timestamp >= cropStart and timestamp <= cropEnd: 117 newEntries.append(entry) 118 119 if rebaseToZero is True: 120 newEntries = [ 121 Point(timeV - cropStart, label) for timeV, label in newEntries 122 ] 123 minT = 0.0 124 maxT = cropEnd - cropStart 125 else: 126 minT = cropStart 127 maxT = cropEnd 128 129 return PointTier(self.name, newEntries, minT, maxT)
Creates a new tier containing all entries inside the new interval
Arguments:
- cropStart:
- cropEnd:
- mode: Mode is ignored. This parameter is kept for compatibility with IntervalTier.crop()
- rebaseToZero: if True, all entries will have their timestamps subtracted by cropStart.
Returns:
the modified version of the current tier
131 def deleteEntry(self, entry: Point) -> None: 132 """Removes an entry from the entries""" 133 self._entries.pop(self._entries.index(entry))
Removes an entry from the entries
135 def dejitter( 136 self, referenceTier: textgrid_tier.TextgridTier, maxDifference: float = 0.001 137 ) -> "PointTier": 138 """ 139 Set timestamps in this tier to be the same as values in the reference tier 140 141 Timestamps will only be moved if they are less than maxDifference away from the 142 reference time. 143 144 This can be used to correct minor alignment errors between tiers, as made when 145 annotating files manually, etc. 146 147 Args: 148 referenceTier: the IntervalTier or PointTier to use as a reference 149 maxDifference: the maximum amount to allow timestamps to be moved by 150 151 Returns: 152 the modified version of the current tier 153 """ 154 referenceTimestamps = referenceTier.timestamps 155 156 newEntries = [] 157 for time, label in self.entries: 158 timeCompare = min(referenceTimestamps, key=lambda x: abs(x - time)) 159 160 if my_math.lessThanOrEqual(abs(time - timeCompare), maxDifference): 161 time = timeCompare 162 newEntries.append((time, label)) 163 164 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
166 def editTimestamps( 167 self, 168 offset: float, 169 reportingMode: Literal["silence", "warning", "error"] = "warning", 170 ) -> "PointTier": 171 """Modifies all timestamps by a constant amount 172 173 Args: 174 offset: 175 reportingMode: one of "silence", "warning", or "error". This flag 176 determines the behavior if an entries moves outside of minTimestamp 177 or maxTimestamp after being edited 178 179 Returns: 180 the modified version of the current tier 181 """ 182 utils.validateOption( 183 "reportingMode", reportingMode, constants.ErrorReportingMode 184 ) 185 errorReporter = utils.getErrorReporter(reportingMode) 186 187 newEntries: List[Point] = [] 188 for timestamp, label in self.entries: 189 newTimestamp = timestamp + offset 190 utils.checkIsUndershoot(newTimestamp, self.minTimestamp, errorReporter) 191 utils.checkIsOvershoot(newTimestamp, self.maxTimestamp, errorReporter) 192 193 if newTimestamp < 0: 194 continue 195 196 newEntries.append(Point(newTimestamp, label)) 197 198 # Determine new min and max timestamps 199 timeList = [float(point.time) for point in newEntries] 200 newMin = min(timeList) 201 newMax = max(timeList) 202 203 if newMin > self.minTimestamp: 204 newMin = self.minTimestamp 205 206 if newMax < self.maxTimestamp: 207 newMax = self.maxTimestamp 208 209 return PointTier(self.name, newEntries, newMin, newMax)
Modifies all timestamps by a constant amount
Arguments:
- offset:
- reportingMode: one of "silence", "warning", or "error". This flag determines the behavior if an entries moves outside of minTimestamp or maxTimestamp after being edited
Returns:
the modified version of the current tier
211 def getValuesAtPoints( 212 self, 213 dataTupleList: List[Tuple[float, ...]], 214 fuzzyMatching: bool = False, 215 ) -> List[Tuple[Any, ...]]: 216 """Get the values that occur at points in the point tier 217 218 The procedure assumes that all data is ordered in time. 219 dataTupleList should be in the form 220 [(t1, v1a, v1b, ..), (t2, v2a, v2b, ..), ..] 221 222 It returns the data in the form of 223 [(t1, v1a, v1b, ..), (t2, v2a, v2b), ..] 224 225 The procedure makes one pass through dataTupleList and one 226 pass through self.entries. If the data is not sequentially 227 ordered, the incorrect response will be returned. 228 229 Args: 230 dataTupleList: 231 fuzzyMatching: if True, if there is not a feature value 232 at a point, the nearest feature value will be taken. 233 234 Returns: 235 A list of values that exist at the given timepoints 236 """ 237 238 currentIndex = 0 239 retList = [] 240 241 sortedDataTupleList = sorted(dataTupleList) 242 for timestamp, label in self.entries: 243 retTuple = utils.getValueAtTime( 244 timestamp, 245 sortedDataTupleList, 246 fuzzyMatching=fuzzyMatching, 247 startI=currentIndex, 248 ) 249 retRow, currentIndex = retTuple 250 retList.append(retRow) 251 252 return retList
Get the values that occur at points in the point tier
The procedure assumes that all data is ordered in time. dataTupleList should be in the form [(t1, v1a, v1b, ..), (t2, v2a, v2b, ..), ..]
It returns the data in the form of [(t1, v1a, v1b, ..), (t2, v2a, v2b), ..]
The procedure makes one pass through dataTupleList and one pass through self.entries. If the data is not sequentially ordered, the incorrect response will be returned.
Arguments:
- dataTupleList:
- fuzzyMatching: if True, if there is not a feature value at a point, the nearest feature value will be taken.
Returns:
A list of values that exist at the given timepoints
254 def eraseRegion( 255 self, 256 start: float, 257 end: float, 258 collisionMode: Literal["truncate", "categorical", "error"] = "error", 259 doShrink: bool = True, 260 ) -> "PointTier": 261 """Makes a region in a tier blank (removes all contained entries) 262 263 Args: 264 start: the start of the deletion interval 265 end: the end of the deletion interval 266 collisionMode: Ignored for the moment (added for compatibility with 267 eraseRegion() for Interval Tiers) 268 doShrink: if True, moves leftward by (/end/ - /start/) all points 269 to the right of /end/ 270 271 Returns: 272 The modified version of the current tier 273 """ 274 275 newTier = self.new() 276 croppedTier = newTier.crop(start, end, constants.CropCollision.TRUNCATED, False) 277 matchList = croppedTier.entries 278 279 if len(matchList) > 0: 280 # Remove all the matches from the entries 281 # Go in reverse order because we're destructively altering 282 # the order of the list (messes up index order) 283 for point in matchList[::-1]: 284 newTier.deleteEntry(point) 285 286 if doShrink is True: 287 newEntries = [] 288 diff = end - start 289 for point in newTier.entries: 290 if point.time < start: 291 newEntries.append(point) 292 elif point.time > end: 293 newEntries.append(Point(point.time - diff, point.label)) 294 295 newMax = newTier.maxTimestamp - diff 296 newTier = newTier.new(entries=newEntries, maxTimestamp=newMax) 297 298 return newTier
Makes a region in a tier blank (removes all contained entries)
Arguments:
- start: the start of the deletion interval
- end: the end of the deletion interval
- collisionMode: Ignored for the moment (added for compatibility with eraseRegion() for Interval Tiers)
- doShrink: if True, moves leftward by (/end/ - /start/) all points to the right of /end/
Returns:
The modified version of the current tier
300 def insertEntry( 301 self, 302 entry: Point, 303 collisionMode: Literal["replace", "merge", "error"] = "error", 304 collisionReportingMode: Literal["silence", "warning"] = "warning", 305 ) -> None: 306 """Inserts an interval into the tier 307 308 Args: 309 entry: the entry to insert 310 collisionMode: determines the behavior if intervals exist in 311 the insertion area. 312 - 'replace', existing items will be removed 313 - 'merge', inserting item will be fused with existing items 314 - 'error', will throw TextgridCollisionException 315 collisionReportingMode: 316 317 Returns: 318 None 319 """ 320 321 utils.validateOption( 322 "collisionMode", collisionMode, constants.IntervalCollision 323 ) 324 utils.validateOption( 325 "collisionReportingMode", 326 collisionReportingMode, 327 constants.ErrorReportingMode, 328 ) 329 collisionReporter = utils.getErrorReporter(collisionReportingMode) 330 331 if not isinstance(entry, Point): 332 newPoint = Point(entry[0], entry[1]) 333 else: 334 newPoint = entry 335 336 matchList = [] 337 i = None 338 for i, point in enumerate(self.entries): 339 if point.time == newPoint.time: 340 matchList.append(point) 341 break 342 343 if len(matchList) == 0: 344 self._entries.append(newPoint) 345 346 elif collisionMode == constants.IntervalCollision.REPLACE: 347 self.deleteEntry(self.entries[i]) 348 self._entries.append(newPoint) 349 350 elif collisionMode == constants.IntervalCollision.MERGE: 351 oldPoint = self.entries[i] 352 mergedPoint = Point( 353 newPoint.time, "-".join([oldPoint.label, newPoint.label]) 354 ) 355 self.deleteEntry(self._entries[i]) 356 self._entries.append(mergedPoint) 357 358 else: 359 raise errors.CollisionError( 360 f"Attempted to insert interval {point} into tier {self.name} " 361 "of textgrid but overlapping entries " 362 f"{[tuple(interval) for interval in matchList]} " 363 "already exist" 364 ) 365 366 self.sort() 367 368 if len(matchList) != 0: 369 collisionReporter( 370 errors.CollisionError, 371 f"Collision warning for ({point}) with items ({matchList}) of tier '{self.name}'", 372 )
Inserts an interval into the tier
Arguments:
- entry: the entry to insert
- collisionMode: determines the behavior if intervals exist in
the insertion area.
- 'replace', existing items will be removed
- 'merge', inserting item will be fused with existing items
- 'error', will throw TextgridCollisionException
- collisionReportingMode:
Returns:
None
374 def insertSpace( 375 self, 376 start: float, 377 duration: float, 378 _collisionMode: Literal["stretch", "split", "no_change", "error"] = "error", 379 ) -> "PointTier": 380 """Inserts a region into the tier 381 382 Args: 383 start: the start time to insert a space at 384 duration: the duration of the space to insert 385 collisionMode: Ignored for the moment (added for compatibility 386 with insertSpace() for Interval Tiers) 387 388 Returns: 389 PointTier: the modified version of the current tier 390 """ 391 392 newEntries = [] 393 for point in self.entries: 394 if point.time <= start: 395 newEntries.append(point) 396 elif point.time > start: 397 newEntries.append(Point(point.time + duration, point.label)) 398 399 newTier = self.new( 400 entries=newEntries, maxTimestamp=self.maxTimestamp + duration 401 ) 402 403 return newTier
Inserts a region into the tier
Arguments:
- start: the start time to insert a space at
- duration: the duration of the space to insert
- collisionMode: Ignored for the moment (added for compatibility with insertSpace() for Interval Tiers)
Returns:
PointTier: the modified version of the current tier
405 def validate( 406 self, reportingMode: Literal["silence", "warning", "error"] = "warning" 407 ) -> bool: 408 """Validate this tier 409 410 Returns whether the tier is valid or not. If reportingMode is "warning" 411 or "error" this will also print on error or stop execution, respectively. 412 413 Args: 414 reportingMode: Determines the behavior if there is a size difference 415 between the maxTimestamp in the tier and the current textgrid. 416 417 Returns: 418 True if this tier is valid; False otherwise 419 """ 420 utils.validateOption( 421 "reportingMode", reportingMode, constants.ErrorReportingMode 422 ) 423 errorReporter = utils.getErrorReporter(reportingMode) 424 425 isValid = True 426 previousPoint = None 427 for point in self.entries: 428 if previousPoint and previousPoint.time > point.time: 429 isValid = False 430 errorReporter( 431 errors.TextgridStateError, 432 f"Points are not sorted in time: " 433 f"[({previousPoint}), ({point})]", 434 ) 435 436 if utils.checkIsUndershoot(point.time, self.minTimestamp, errorReporter): 437 isValid = False 438 439 if utils.checkIsOvershoot(point.time, self.maxTimestamp, errorReporter): 440 isValid = False 441 442 previousPoint = point 443 444 return isValid
Validate this tier
Returns whether the tier is valid or not. If reportingMode is "warning" or "error" this will also print on error or stop execution, respectively.
Arguments:
- reportingMode: Determines the behavior if there is a size difference between the maxTimestamp in the tier and the current textgrid.
Returns:
True if this tier is valid; False otherwise