praatio.praatio_scripts
Common/generic scripts or utilities that extend the functionality of praatio
see examples/correct_misaligned_tiers.py, examples/delete_vowels.py, examples/extract_subwavs.py, examples/splice_example.py.
1""" 2Common/generic scripts or utilities that extend the functionality of praatio 3 4see **examples/correct_misaligned_tiers.py**, **examples/delete_vowels.py**, 5**examples/extract_subwavs.py**, **examples/splice_example.py**. 6""" 7 8import os 9from os.path import join 10import math 11from typing import Callable, List, Tuple, Optional 12 13from typing_extensions import Literal, Final 14 15from praatio import textgrid 16from praatio import audio 17from praatio.data_classes import textgrid_tier 18from praatio.utilities import constants 19from praatio.utilities.constants import Point, Interval 20from praatio.utilities import errors 21 22 23class NameStyle: 24 APPEND = "append" 25 APPEND_NO_I = "append_no_i" 26 LABEL = "label" 27 28 29def _shiftTimes( 30 tg: textgrid.Textgrid, timeV: float, newTimeV: float 31) -> textgrid.Textgrid: 32 """Change all instances of timeV in the textgrid to newTimeV 33 34 These are meant to be small changes. No checks are done to see 35 if the new interval steps on other intervals 36 """ 37 tg = tg.new() 38 for tier in tg.tiers: 39 if isinstance(tier, textgrid.IntervalTier): 40 entries = [ 41 entry 42 for entry in tier.entries 43 if entry[0] == timeV or entry[1] == timeV 44 ] 45 insertEntries = [] 46 for entry in entries: 47 if entry[0] == timeV: 48 newStart, newStop = newTimeV, entry[1] 49 elif entry[1] == timeV: 50 newStart, newStop = entry[0], newTimeV 51 tier.deleteEntry(entry) 52 insertEntries.append((newStart, newStop, entry[2])) 53 54 for entry in insertEntries: 55 tier.insertEntry(entry) 56 57 elif isinstance(tier, textgrid.PointTier): 58 entries = [entry for entry in tier.entries if entry[0] == timeV] 59 for entry in entries: 60 tier.deleteEntry(entry) 61 tier.insertEntry(Point(newTimeV, entry[1])) 62 63 return tg 64 65 66def audioSplice( 67 audioObj: audio.Wav, 68 spliceSegment: audio.Wav, 69 tg: textgrid.Textgrid, 70 tierName: str, 71 newLabel: str, 72 insertStart: float, 73 insertStop: float = None, 74 alignToZeroCrossing: bool = True, 75) -> Tuple[audio.Wav, textgrid.Textgrid]: 76 """Splices a segment into an audio file and corresponding textgrid 77 78 Args: 79 audioObj: the audio to splice into 80 spliceSegment: the audio segment that will be placed into a 81 larger audio file 82 tg: the textgrid to splice into 83 tierName: the name of the tier that will receive the new label 84 newLabel: the label for the splice interval on the tier with 85 name tierName 86 insertStart: the time to insert the splice 87 insertStop: if not None, will erase the region between 88 sourceSpStart and sourceSpEnd. (In practice this means audioSplice 89 removes one segment and inserts another in its place) 90 alignToZeroCrossing: if True, moves all involved times to the nearest 91 zero crossing in the audio. Generally results 92 in better sounding output 93 94 Returns: 95 [Wav, Textgrid] 96 """ 97 98 retTG = tg.new() 99 100 # Ensure all time points involved in splicing fall on zero crossings 101 if alignToZeroCrossing is True: 102 # Cut the splice segment to zero crossings 103 spliceDuration = spliceSegment.duration 104 spliceZeroStart = spliceSegment.findNearestZeroCrossing(0) 105 spliceZeroEnd = spliceSegment.findNearestZeroCrossing(spliceDuration) 106 spliceSegment = spliceSegment.getSubwav(spliceZeroStart, spliceZeroEnd) 107 108 # Move the textgrid borders to zero crossings 109 oldInsertStart = insertStart 110 insertStart = audioObj.findNearestZeroCrossing(oldInsertStart) 111 retTG = _shiftTimes(retTG, oldInsertStart, insertStart) 112 113 if insertStop is not None: 114 oldInsertStop = insertStop 115 insertStop = audioObj.findNearestZeroCrossing(oldInsertStop) 116 retTG = _shiftTimes(retTG, oldInsertStop, insertStop) 117 118 # Get the start time 119 insertTime = insertStart 120 if insertStop is not None: 121 insertTime = insertStop 122 123 # Insert into the audio file 124 audioObj.insert(insertTime, spliceSegment.frames) 125 126 # Insert a blank region into the textgrid on all tiers 127 targetDuration = spliceSegment.duration 128 retTG = retTG.insertSpace(insertTime, targetDuration, "stretch") 129 130 # Insert the splice entry into the target tier 131 newEntry = (insertTime, insertTime + targetDuration, newLabel) 132 retTG.getTier(tierName).insertEntry(newEntry) 133 134 # Finally, delete the old section if requested 135 if insertStop is not None: 136 audioObj.deleteSegment(insertStart, insertStop) 137 retTG = retTG.eraseRegion(insertStart, insertStop, doShrink=True) 138 139 return audioObj, retTG 140 141 142def spellCheckEntries( 143 tg: textgrid.Textgrid, 144 targetTierName: str, 145 newTierName: str, 146 checkFunction: Callable[[str], bool], 147 printEntries: bool = False, 148) -> textgrid.Textgrid: 149 """Spell checks words in a textgrid 150 151 Entries can contain one or more words, separated by whitespace. 152 If a mispelling is found, it is noted in a special tier and optionally 153 printed to the screen. 154 155 checkFunction is user-defined. It takes a word and returns True if it is 156 spelled correctly and false if not. There are robust spell check libraries 157 for python like woosh or pyenchant. I have already written a naive 158 spell checker in the pysle.praattools library. 159 160 Args: 161 checkFunction: should return True if a word is spelled correctly and 162 False otherwise 163 """ 164 punctuationList = [ 165 "_", 166 ",", 167 "'", 168 '"', 169 "!", 170 "?", 171 ".", 172 ";", 173 ] 174 175 tg = tg.new() 176 tier = tg.getTier(targetTierName) 177 178 mispelledEntries = [] 179 for start, end, label in tier.entries: 180 # Remove punctuation 181 for char in punctuationList: 182 label = label.replace(char, "") 183 184 wordList = label.split() 185 mispelledList = [] 186 for word in wordList: 187 if not checkFunction(word): 188 mispelledList.append(word) 189 190 if len(mispelledList) > 0: 191 mispelledTxt = ", ".join(mispelledList) 192 mispelledEntries.append(Interval(start, end, mispelledTxt)) 193 194 if printEntries is True: 195 print((start, end, mispelledTxt)) 196 197 tier = textgrid.IntervalTier( 198 newTierName, mispelledEntries, tg.minTimestamp, tg.maxTimestamp 199 ) 200 tg.addTier(tier) 201 202 return tg 203 204 205def splitTierEntries( 206 tg: textgrid.Textgrid, 207 sourceTierName: str, 208 targetTierName: str, 209 startT: float = None, 210 endT: float = None, 211) -> textgrid.Textgrid: 212 """Split each entry in a tier by space 213 214 The split entries will be placed on a new tier. The split entries 215 are equally allocated a subsegment of the interval occupied by the 216 source tier. e.g. [(63, 66, 'the blue cat'), ] would become 217 [(63, 64, 'the'), (64, 65, 'blue'), (65, 66, 'cat'), ] 218 219 This could be used to decompose utterances into words or, with pysle, 220 words into phones. 221 """ 222 minT = tg.minTimestamp 223 maxT = tg.maxTimestamp 224 225 sourceTier = tg.getTier(sourceTierName) 226 targetTier = None 227 228 # Examine a subset of the source tier? 229 if startT is not None or endT is not None: 230 if startT is None: 231 startT = minT 232 if endT is None: 233 endT = maxT 234 235 sourceTier = sourceTier.crop(startT, endT, "truncated", False) 236 237 if targetTierName in tg.tierNames: 238 targetTier = tg.getTier(targetTierName) 239 targetTier = targetTier.eraseRegion(startT, endT, "truncate", False) 240 241 # Split the entries in the source tier 242 newEntries = [] 243 for start, end, label in sourceTier.entries: 244 labelList = label.split() 245 intervalLength = (end - start) / float(len(labelList)) 246 247 newSubEntries = [ 248 Interval( 249 start + intervalLength * i, start + intervalLength * (i + 1), label 250 ) 251 for i, label in enumerate(labelList) 252 ] 253 newEntries.extend(newSubEntries) 254 255 # Create a new tier 256 if targetTier is None: 257 targetTier = textgrid.IntervalTier(targetTierName, newEntries, minT, maxT) 258 259 # Or insert new entries into existing target tier 260 else: 261 for entry in newEntries: 262 targetTier.insertEntry(entry, constants.IntervalCollision.ERROR) 263 264 # Insert the tier into the textgrid 265 if targetTierName in tg.tierNames: 266 tg.removeTier(targetTierName) 267 tg.addTier(targetTier) 268 269 return tg 270 271 272def tgBoundariesToZeroCrossings( 273 tg: textgrid.Textgrid, 274 wav: audio.Wav, 275 adjustPointTiers: bool = True, 276 adjustIntervalTiers: bool = True, 277) -> textgrid.Textgrid: 278 """Makes all textgrid interval boundaries fall on pressure wave zero crossings 279 280 adjustPointTiers: if True, point tiers will be adjusted. 281 adjustIntervalTiers: if True, interval tiers will be adjusted. 282 """ 283 for tier in tg.tiers: 284 newTier: textgrid_tier.TextgridTier 285 if isinstance(tier, textgrid.PointTier): 286 if adjustPointTiers is False: 287 continue 288 289 points = [] 290 for start, label in tier.entries: 291 newStart = wav.findNearestZeroCrossing(start) 292 points.append(Point(newStart, label)) 293 newTier = tier.new(entries=points) 294 elif isinstance(tier, textgrid.IntervalTier): 295 if adjustIntervalTiers is False: 296 continue 297 298 intervals = [] 299 for start, end, label in tier.entries: 300 newStart = wav.findNearestZeroCrossing(start) 301 newStop = wav.findNearestZeroCrossing(end) 302 intervals.append(Interval(newStart, newStop, label)) 303 newTier = tier.new(entries=intervals) 304 305 tg.replaceTier(tier.name, newTier) 306 307 return tg 308 309 310def splitAudioOnTier( 311 wavFN: str, 312 tgFN: str, 313 tierName: str, 314 outputPath: str, 315 outputTGFlag: bool = False, 316 nameStyle: Optional[Literal["append", "append_no_i", "label"]] = None, 317 noPartialIntervals: bool = False, 318 silenceLabel: str = None, 319) -> List[Tuple[float, float, str]]: 320 """Outputs one subwav for each entry in the tier of a textgrid 321 322 Args: 323 wavnFN: 324 tgFN: 325 tierName: 326 outputPath: 327 outputTGFlag: If True, outputs paired, cropped textgrids 328 If is type str (a tier name), outputs a paired, cropped 329 textgrid with only the specified tier 330 nameStyle: 331 - 'append': append interval label to output name 332 - 'append_no_i': append label but not interval to output name 333 - 'label': output name is the same as label 334 - None: output name plus the interval number 335 noPartialIntervals: if True: intervals in non-target tiers that 336 are not wholly contained by an interval in the target tier will not 337 be included in the output textgrids 338 silenceLabel: the label for silent regions. If silences are 339 unlabeled intervals (i.e. blank) then leave this alone. If 340 silences are labeled using praat's "annotate >> to silences" 341 then this value should be "silences" 342 """ 343 if not os.path.exists(outputPath): 344 os.mkdir(outputPath) 345 346 def getValue(myBool) -> Literal["strict", "lax", "truncated"]: 347 # This will make mypy happy 348 if myBool: 349 return constants.CropCollision.STRICT 350 else: 351 return constants.CropCollision.TRUNCATED 352 353 mode: Final = getValue(noPartialIntervals) 354 355 tg = textgrid.openTextgrid(tgFN, False) 356 entries = tg.getTier(tierName).entries 357 358 if silenceLabel is not None: 359 entries = [entry for entry in entries if entry.label != silenceLabel] 360 361 # Build the output name template 362 name = os.path.splitext(os.path.split(wavFN)[1])[0] 363 orderOfMagnitude = int(math.floor(math.log10(len(entries)))) 364 365 # We want one more zero in the output than the order of magnitude 366 outputTemplate = "%s_%%0%dd" % (name, orderOfMagnitude + 1) 367 368 firstWarning = True 369 370 # If we're using the 'label' namestyle for outputs, all of the 371 # interval labels have to be unique, or wave files with those 372 # labels as names, will be overwritten 373 if nameStyle == NameStyle.LABEL: 374 wordList = [interval.label for interval in entries] 375 multipleInstList = [] 376 for word in set(wordList): 377 if wordList.count(word) > 1: 378 multipleInstList.append(word) 379 380 if len(multipleInstList) > 0: 381 instListTxt = "\n".join(multipleInstList) 382 print( 383 f"Overwriting wave files in: {outputPath}\n" 384 f"Intervals exist with the same name:\n{instListTxt}" 385 ) 386 firstWarning = False 387 388 # Output wave files 389 outputFNList = [] 390 wavQObj = audio.QueryWav(wavFN) 391 for i, entry in enumerate(entries): 392 start, end, label = entry 393 394 # Resolve output name 395 if nameStyle == NameStyle.APPEND_NO_I: 396 outputName = f"{name}_{label}" 397 elif nameStyle == NameStyle.LABEL: 398 outputName = label 399 else: 400 outputName = outputTemplate % i 401 if nameStyle == NameStyle.APPEND: 402 outputName += f"_{label}" 403 404 outputFNFullPath = join(outputPath, outputName + ".wav") 405 406 if os.path.exists(outputFNFullPath) and firstWarning: 407 print( 408 f"Overwriting wave files in: {outputPath}\n" 409 "Files existed before or intervals exist with " 410 f"the same name:\n{outputName}" 411 ) 412 413 frames = wavQObj.getFrames(start, end) 414 wavQObj.outputFrames(frames, outputFNFullPath) 415 416 outputFNList.append((start, end, outputName + ".wav")) 417 418 # Output the textgrid if requested 419 if outputTGFlag is not False: 420 subTG = tg.crop(start, end, mode, True) 421 422 if isinstance(outputTGFlag, str): 423 for tierName in subTG.tierNames: 424 if tierName != outputTGFlag: 425 subTG.removeTier(tierName) 426 427 subTG.save( 428 join(outputPath, outputName + ".TextGrid"), "short_textgrid", True 429 ) 430 431 return outputFNList 432 433 434# TODO: Remove this method in the next major version 435# Migrate to using the new Textgridtier.dejitter() 436def alignBoundariesAcrossTiers( 437 tg: textgrid.Textgrid, tierName: str, maxDifference: float = 0.005 438) -> textgrid.Textgrid: 439 """Aligns boundaries or points in a textgrid that suffer from 'jitter' 440 441 Often times, boundaries in different tiers are meant to line up. 442 For example the boundary of the first phone in a word and the start 443 of the word. If manually annotated, however, those values might 444 not be the same, even if they were intended to be the same. 445 446 This script will force all boundaries within /maxDifference/ amount 447 to be the same value. The replacement value is either the majority 448 value found within /maxDifference/ or, if no majority exists, than 449 the value used in the search query. 450 451 Args: 452 tg: the textgrid to operate on 453 tierName: the name of the reference tier to compare other tiers against 454 maxDifference: any boundaries that differ less this amount compared 455 to boundaries in the reference tier will be adjusted 456 457 Returns: 458 the provided textgrid with aligned boundaries 459 460 Raises: 461 ArgumentError: The provided maxDifference is larger than the smallest difference in 462 the tier to be used for comparisons, which could lead to strange results. 463 In such a case, choose a smaller maxDifference. 464 """ 465 referenceTier = tg.getTier(tierName) 466 times = referenceTier.timestamps 467 468 for time, nextTime in zip(times[1::], times[2::]): 469 if nextTime - time < maxDifference: 470 raise errors.ArgumentError( 471 "The provided maxDifference is larger than the smallest difference in" 472 "the tier used for comparison, which could lead to strange results." 473 "Please choose a smaller maxDifference.\n" 474 f"Max difference: {maxDifference}\n" 475 f"found difference {nextTime - time} for times {time} and {nextTime}" 476 ) 477 478 for tier in tg.tiers: 479 if tier.name == tierName: 480 continue 481 482 tier = tier.dejitter(referenceTier, maxDifference) 483 tg.replaceTier(tier.name, tier) 484 485 return tg
67def audioSplice( 68 audioObj: audio.Wav, 69 spliceSegment: audio.Wav, 70 tg: textgrid.Textgrid, 71 tierName: str, 72 newLabel: str, 73 insertStart: float, 74 insertStop: float = None, 75 alignToZeroCrossing: bool = True, 76) -> Tuple[audio.Wav, textgrid.Textgrid]: 77 """Splices a segment into an audio file and corresponding textgrid 78 79 Args: 80 audioObj: the audio to splice into 81 spliceSegment: the audio segment that will be placed into a 82 larger audio file 83 tg: the textgrid to splice into 84 tierName: the name of the tier that will receive the new label 85 newLabel: the label for the splice interval on the tier with 86 name tierName 87 insertStart: the time to insert the splice 88 insertStop: if not None, will erase the region between 89 sourceSpStart and sourceSpEnd. (In practice this means audioSplice 90 removes one segment and inserts another in its place) 91 alignToZeroCrossing: if True, moves all involved times to the nearest 92 zero crossing in the audio. Generally results 93 in better sounding output 94 95 Returns: 96 [Wav, Textgrid] 97 """ 98 99 retTG = tg.new() 100 101 # Ensure all time points involved in splicing fall on zero crossings 102 if alignToZeroCrossing is True: 103 # Cut the splice segment to zero crossings 104 spliceDuration = spliceSegment.duration 105 spliceZeroStart = spliceSegment.findNearestZeroCrossing(0) 106 spliceZeroEnd = spliceSegment.findNearestZeroCrossing(spliceDuration) 107 spliceSegment = spliceSegment.getSubwav(spliceZeroStart, spliceZeroEnd) 108 109 # Move the textgrid borders to zero crossings 110 oldInsertStart = insertStart 111 insertStart = audioObj.findNearestZeroCrossing(oldInsertStart) 112 retTG = _shiftTimes(retTG, oldInsertStart, insertStart) 113 114 if insertStop is not None: 115 oldInsertStop = insertStop 116 insertStop = audioObj.findNearestZeroCrossing(oldInsertStop) 117 retTG = _shiftTimes(retTG, oldInsertStop, insertStop) 118 119 # Get the start time 120 insertTime = insertStart 121 if insertStop is not None: 122 insertTime = insertStop 123 124 # Insert into the audio file 125 audioObj.insert(insertTime, spliceSegment.frames) 126 127 # Insert a blank region into the textgrid on all tiers 128 targetDuration = spliceSegment.duration 129 retTG = retTG.insertSpace(insertTime, targetDuration, "stretch") 130 131 # Insert the splice entry into the target tier 132 newEntry = (insertTime, insertTime + targetDuration, newLabel) 133 retTG.getTier(tierName).insertEntry(newEntry) 134 135 # Finally, delete the old section if requested 136 if insertStop is not None: 137 audioObj.deleteSegment(insertStart, insertStop) 138 retTG = retTG.eraseRegion(insertStart, insertStop, doShrink=True) 139 140 return audioObj, retTG
Splices a segment into an audio file and corresponding textgrid
Arguments:
- audioObj: the audio to splice into
- spliceSegment: the audio segment that will be placed into a larger audio file
- tg: the textgrid to splice into
- tierName: the name of the tier that will receive the new label
- newLabel: the label for the splice interval on the tier with name tierName
- insertStart: the time to insert the splice
- insertStop: if not None, will erase the region between sourceSpStart and sourceSpEnd. (In practice this means audioSplice removes one segment and inserts another in its place)
- alignToZeroCrossing: if True, moves all involved times to the nearest zero crossing in the audio. Generally results in better sounding output
Returns:
[Wav, Textgrid]
143def spellCheckEntries( 144 tg: textgrid.Textgrid, 145 targetTierName: str, 146 newTierName: str, 147 checkFunction: Callable[[str], bool], 148 printEntries: bool = False, 149) -> textgrid.Textgrid: 150 """Spell checks words in a textgrid 151 152 Entries can contain one or more words, separated by whitespace. 153 If a mispelling is found, it is noted in a special tier and optionally 154 printed to the screen. 155 156 checkFunction is user-defined. It takes a word and returns True if it is 157 spelled correctly and false if not. There are robust spell check libraries 158 for python like woosh or pyenchant. I have already written a naive 159 spell checker in the pysle.praattools library. 160 161 Args: 162 checkFunction: should return True if a word is spelled correctly and 163 False otherwise 164 """ 165 punctuationList = [ 166 "_", 167 ",", 168 "'", 169 '"', 170 "!", 171 "?", 172 ".", 173 ";", 174 ] 175 176 tg = tg.new() 177 tier = tg.getTier(targetTierName) 178 179 mispelledEntries = [] 180 for start, end, label in tier.entries: 181 # Remove punctuation 182 for char in punctuationList: 183 label = label.replace(char, "") 184 185 wordList = label.split() 186 mispelledList = [] 187 for word in wordList: 188 if not checkFunction(word): 189 mispelledList.append(word) 190 191 if len(mispelledList) > 0: 192 mispelledTxt = ", ".join(mispelledList) 193 mispelledEntries.append(Interval(start, end, mispelledTxt)) 194 195 if printEntries is True: 196 print((start, end, mispelledTxt)) 197 198 tier = textgrid.IntervalTier( 199 newTierName, mispelledEntries, tg.minTimestamp, tg.maxTimestamp 200 ) 201 tg.addTier(tier) 202 203 return tg
Spell checks words in a textgrid
Entries can contain one or more words, separated by whitespace. If a mispelling is found, it is noted in a special tier and optionally printed to the screen.
checkFunction is user-defined. It takes a word and returns True if it is spelled correctly and false if not. There are robust spell check libraries for python like woosh or pyenchant. I have already written a naive spell checker in the pysle.praattools library.
Arguments:
- checkFunction: should return True if a word is spelled correctly and False otherwise
206def splitTierEntries( 207 tg: textgrid.Textgrid, 208 sourceTierName: str, 209 targetTierName: str, 210 startT: float = None, 211 endT: float = None, 212) -> textgrid.Textgrid: 213 """Split each entry in a tier by space 214 215 The split entries will be placed on a new tier. The split entries 216 are equally allocated a subsegment of the interval occupied by the 217 source tier. e.g. [(63, 66, 'the blue cat'), ] would become 218 [(63, 64, 'the'), (64, 65, 'blue'), (65, 66, 'cat'), ] 219 220 This could be used to decompose utterances into words or, with pysle, 221 words into phones. 222 """ 223 minT = tg.minTimestamp 224 maxT = tg.maxTimestamp 225 226 sourceTier = tg.getTier(sourceTierName) 227 targetTier = None 228 229 # Examine a subset of the source tier? 230 if startT is not None or endT is not None: 231 if startT is None: 232 startT = minT 233 if endT is None: 234 endT = maxT 235 236 sourceTier = sourceTier.crop(startT, endT, "truncated", False) 237 238 if targetTierName in tg.tierNames: 239 targetTier = tg.getTier(targetTierName) 240 targetTier = targetTier.eraseRegion(startT, endT, "truncate", False) 241 242 # Split the entries in the source tier 243 newEntries = [] 244 for start, end, label in sourceTier.entries: 245 labelList = label.split() 246 intervalLength = (end - start) / float(len(labelList)) 247 248 newSubEntries = [ 249 Interval( 250 start + intervalLength * i, start + intervalLength * (i + 1), label 251 ) 252 for i, label in enumerate(labelList) 253 ] 254 newEntries.extend(newSubEntries) 255 256 # Create a new tier 257 if targetTier is None: 258 targetTier = textgrid.IntervalTier(targetTierName, newEntries, minT, maxT) 259 260 # Or insert new entries into existing target tier 261 else: 262 for entry in newEntries: 263 targetTier.insertEntry(entry, constants.IntervalCollision.ERROR) 264 265 # Insert the tier into the textgrid 266 if targetTierName in tg.tierNames: 267 tg.removeTier(targetTierName) 268 tg.addTier(targetTier) 269 270 return tg
Split each entry in a tier by space
The split entries will be placed on a new tier. The split entries are equally allocated a subsegment of the interval occupied by the source tier. e.g. [(63, 66, 'the blue cat'), ] would become [(63, 64, 'the'), (64, 65, 'blue'), (65, 66, 'cat'), ]
This could be used to decompose utterances into words or, with pysle, words into phones.
273def tgBoundariesToZeroCrossings( 274 tg: textgrid.Textgrid, 275 wav: audio.Wav, 276 adjustPointTiers: bool = True, 277 adjustIntervalTiers: bool = True, 278) -> textgrid.Textgrid: 279 """Makes all textgrid interval boundaries fall on pressure wave zero crossings 280 281 adjustPointTiers: if True, point tiers will be adjusted. 282 adjustIntervalTiers: if True, interval tiers will be adjusted. 283 """ 284 for tier in tg.tiers: 285 newTier: textgrid_tier.TextgridTier 286 if isinstance(tier, textgrid.PointTier): 287 if adjustPointTiers is False: 288 continue 289 290 points = [] 291 for start, label in tier.entries: 292 newStart = wav.findNearestZeroCrossing(start) 293 points.append(Point(newStart, label)) 294 newTier = tier.new(entries=points) 295 elif isinstance(tier, textgrid.IntervalTier): 296 if adjustIntervalTiers is False: 297 continue 298 299 intervals = [] 300 for start, end, label in tier.entries: 301 newStart = wav.findNearestZeroCrossing(start) 302 newStop = wav.findNearestZeroCrossing(end) 303 intervals.append(Interval(newStart, newStop, label)) 304 newTier = tier.new(entries=intervals) 305 306 tg.replaceTier(tier.name, newTier) 307 308 return tg
Makes all textgrid interval boundaries fall on pressure wave zero crossings
adjustPointTiers: if True, point tiers will be adjusted. adjustIntervalTiers: if True, interval tiers will be adjusted.
311def splitAudioOnTier( 312 wavFN: str, 313 tgFN: str, 314 tierName: str, 315 outputPath: str, 316 outputTGFlag: bool = False, 317 nameStyle: Optional[Literal["append", "append_no_i", "label"]] = None, 318 noPartialIntervals: bool = False, 319 silenceLabel: str = None, 320) -> List[Tuple[float, float, str]]: 321 """Outputs one subwav for each entry in the tier of a textgrid 322 323 Args: 324 wavnFN: 325 tgFN: 326 tierName: 327 outputPath: 328 outputTGFlag: If True, outputs paired, cropped textgrids 329 If is type str (a tier name), outputs a paired, cropped 330 textgrid with only the specified tier 331 nameStyle: 332 - 'append': append interval label to output name 333 - 'append_no_i': append label but not interval to output name 334 - 'label': output name is the same as label 335 - None: output name plus the interval number 336 noPartialIntervals: if True: intervals in non-target tiers that 337 are not wholly contained by an interval in the target tier will not 338 be included in the output textgrids 339 silenceLabel: the label for silent regions. If silences are 340 unlabeled intervals (i.e. blank) then leave this alone. If 341 silences are labeled using praat's "annotate >> to silences" 342 then this value should be "silences" 343 """ 344 if not os.path.exists(outputPath): 345 os.mkdir(outputPath) 346 347 def getValue(myBool) -> Literal["strict", "lax", "truncated"]: 348 # This will make mypy happy 349 if myBool: 350 return constants.CropCollision.STRICT 351 else: 352 return constants.CropCollision.TRUNCATED 353 354 mode: Final = getValue(noPartialIntervals) 355 356 tg = textgrid.openTextgrid(tgFN, False) 357 entries = tg.getTier(tierName).entries 358 359 if silenceLabel is not None: 360 entries = [entry for entry in entries if entry.label != silenceLabel] 361 362 # Build the output name template 363 name = os.path.splitext(os.path.split(wavFN)[1])[0] 364 orderOfMagnitude = int(math.floor(math.log10(len(entries)))) 365 366 # We want one more zero in the output than the order of magnitude 367 outputTemplate = "%s_%%0%dd" % (name, orderOfMagnitude + 1) 368 369 firstWarning = True 370 371 # If we're using the 'label' namestyle for outputs, all of the 372 # interval labels have to be unique, or wave files with those 373 # labels as names, will be overwritten 374 if nameStyle == NameStyle.LABEL: 375 wordList = [interval.label for interval in entries] 376 multipleInstList = [] 377 for word in set(wordList): 378 if wordList.count(word) > 1: 379 multipleInstList.append(word) 380 381 if len(multipleInstList) > 0: 382 instListTxt = "\n".join(multipleInstList) 383 print( 384 f"Overwriting wave files in: {outputPath}\n" 385 f"Intervals exist with the same name:\n{instListTxt}" 386 ) 387 firstWarning = False 388 389 # Output wave files 390 outputFNList = [] 391 wavQObj = audio.QueryWav(wavFN) 392 for i, entry in enumerate(entries): 393 start, end, label = entry 394 395 # Resolve output name 396 if nameStyle == NameStyle.APPEND_NO_I: 397 outputName = f"{name}_{label}" 398 elif nameStyle == NameStyle.LABEL: 399 outputName = label 400 else: 401 outputName = outputTemplate % i 402 if nameStyle == NameStyle.APPEND: 403 outputName += f"_{label}" 404 405 outputFNFullPath = join(outputPath, outputName + ".wav") 406 407 if os.path.exists(outputFNFullPath) and firstWarning: 408 print( 409 f"Overwriting wave files in: {outputPath}\n" 410 "Files existed before or intervals exist with " 411 f"the same name:\n{outputName}" 412 ) 413 414 frames = wavQObj.getFrames(start, end) 415 wavQObj.outputFrames(frames, outputFNFullPath) 416 417 outputFNList.append((start, end, outputName + ".wav")) 418 419 # Output the textgrid if requested 420 if outputTGFlag is not False: 421 subTG = tg.crop(start, end, mode, True) 422 423 if isinstance(outputTGFlag, str): 424 for tierName in subTG.tierNames: 425 if tierName != outputTGFlag: 426 subTG.removeTier(tierName) 427 428 subTG.save( 429 join(outputPath, outputName + ".TextGrid"), "short_textgrid", True 430 ) 431 432 return outputFNList
Outputs one subwav for each entry in the tier of a textgrid
Arguments:
- wavnFN:
- tgFN:
- tierName:
- outputPath:
- outputTGFlag: If True, outputs paired, cropped textgrids If is type str (a tier name), outputs a paired, cropped textgrid with only the specified tier
- nameStyle: - 'append': append interval label to output name
- 'append_no_i': append label but not interval to output name
- 'label': output name is the same as label
- None: output name plus the interval number
- noPartialIntervals: if True: intervals in non-target tiers that are not wholly contained by an interval in the target tier will not be included in the output textgrids
- silenceLabel: the label for silent regions. If silences are unlabeled intervals (i.e. blank) then leave this alone. If silences are labeled using praat's "annotate >> to silences" then this value should be "silences"
437def alignBoundariesAcrossTiers( 438 tg: textgrid.Textgrid, tierName: str, maxDifference: float = 0.005 439) -> textgrid.Textgrid: 440 """Aligns boundaries or points in a textgrid that suffer from 'jitter' 441 442 Often times, boundaries in different tiers are meant to line up. 443 For example the boundary of the first phone in a word and the start 444 of the word. If manually annotated, however, those values might 445 not be the same, even if they were intended to be the same. 446 447 This script will force all boundaries within /maxDifference/ amount 448 to be the same value. The replacement value is either the majority 449 value found within /maxDifference/ or, if no majority exists, than 450 the value used in the search query. 451 452 Args: 453 tg: the textgrid to operate on 454 tierName: the name of the reference tier to compare other tiers against 455 maxDifference: any boundaries that differ less this amount compared 456 to boundaries in the reference tier will be adjusted 457 458 Returns: 459 the provided textgrid with aligned boundaries 460 461 Raises: 462 ArgumentError: The provided maxDifference is larger than the smallest difference in 463 the tier to be used for comparisons, which could lead to strange results. 464 In such a case, choose a smaller maxDifference. 465 """ 466 referenceTier = tg.getTier(tierName) 467 times = referenceTier.timestamps 468 469 for time, nextTime in zip(times[1::], times[2::]): 470 if nextTime - time < maxDifference: 471 raise errors.ArgumentError( 472 "The provided maxDifference is larger than the smallest difference in" 473 "the tier used for comparison, which could lead to strange results." 474 "Please choose a smaller maxDifference.\n" 475 f"Max difference: {maxDifference}\n" 476 f"found difference {nextTime - time} for times {time} and {nextTime}" 477 ) 478 479 for tier in tg.tiers: 480 if tier.name == tierName: 481 continue 482 483 tier = tier.dejitter(referenceTier, maxDifference) 484 tg.replaceTier(tier.name, tier) 485 486 return tg
Aligns boundaries or points in a textgrid that suffer from 'jitter'
Often times, boundaries in different tiers are meant to line up. For example the boundary of the first phone in a word and the start of the word. If manually annotated, however, those values might not be the same, even if they were intended to be the same.
This script will force all boundaries within /maxDifference/ amount to be the same value. The replacement value is either the majority value found within /maxDifference/ or, if no majority exists, than the value used in the search query.
Arguments:
- tg: the textgrid to operate on
- tierName: the name of the reference tier to compare other tiers against
- maxDifference: any boundaries that differ less this amount compared to boundaries in the reference tier will be adjusted
Returns:
the provided textgrid with aligned boundaries
Raises:
- ArgumentError: The provided maxDifference is larger than the smallest difference in the tier to be used for comparisons, which could lead to strange results. In such a case, choose a smaller maxDifference.