praatio.praatio_scripts API documentation

# 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.

View Source

      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: View Source

    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]: View Source

     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: View Source

    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: View Source

    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: View Source

    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]]: View Source

    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: View Source

    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.