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
class NameStyle:
24class NameStyle:
25    APPEND = "append"
26    APPEND_NO_I = "append_no_i"
27    LABEL = "label"
APPEND = 'append'
APPEND_NO_I = 'append_no_i'
LABEL = 'label'
def audioSplice( audioObj: praatio.audio.Wav, spliceSegment: praatio.audio.Wav, tg: praatio.data_classes.textgrid.Textgrid, tierName: str, newLabel: str, insertStart: float, insertStop: float = None, alignToZeroCrossing: bool = True) -> Tuple[praatio.audio.Wav, praatio.data_classes.textgrid.Textgrid]:
 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]

def spellCheckEntries( tg: praatio.data_classes.textgrid.Textgrid, targetTierName: str, newTierName: str, checkFunction: Callable[[str], bool], printEntries: bool = False) -> praatio.data_classes.textgrid.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
def splitTierEntries( tg: praatio.data_classes.textgrid.Textgrid, sourceTierName: str, targetTierName: str, startT: float = None, endT: float = None) -> praatio.data_classes.textgrid.Textgrid:
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.

def tgBoundariesToZeroCrossings( tg: praatio.data_classes.textgrid.Textgrid, wav: praatio.audio.Wav, adjustPointTiers: bool = True, adjustIntervalTiers: bool = True) -> praatio.data_classes.textgrid.Textgrid:
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.

def splitAudioOnTier( wavFN: str, tgFN: str, tierName: str, outputPath: str, outputTGFlag: bool = False, nameStyle: Optional[Literal['append', 'append_no_i', 'label']] = None, noPartialIntervals: bool = False, silenceLabel: str = None) -> List[Tuple[float, float, str]]:
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"
def alignBoundariesAcrossTiers( tg: praatio.data_classes.textgrid.Textgrid, tierName: str, maxDifference: float = 0.005) -> praatio.data_classes.textgrid.Textgrid:
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.