Skip to content

Commit

Permalink
Merge pull request #32 from NickleDave/add-textgrid
Browse files Browse the repository at this point in the history
Add textgrid
  • Loading branch information
NickleDave committed May 5, 2019
2 parents 26a8ae6 + c82df72 commit 6238925
Show file tree
Hide file tree
Showing 32 changed files with 9,204 additions and 37 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
'crowsetta.format': [
'notmat = crowsetta.notmat',
'koumura = crowsetta.koumura',
'textgrid = crowsetta.textgrid',
]
}

Expand Down
6 changes: 4 additions & 2 deletions src/crowsetta/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from . import notmat
from . import koumura
from .transcriber import Transcriber
from .segment import Segment
from .sequence import Sequence
Expand All @@ -8,3 +6,7 @@
from . import data
from . import formats

# built-in formats
from . import notmat
from . import koumura
from . import textgrid
29 changes: 3 additions & 26 deletions src/crowsetta/notmat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
produced by evsonganaly GUI
"""
import os
from pathlib import Path

import numpy as np
import scipy.io
Expand All @@ -11,29 +10,7 @@
from .sequence import Sequence
from .csv import seq2csv
from .meta import Meta


def _parse_file(file):
"""helper function that parses/validates value for file argument;
puts a single string or Path into a list to iterate over it (cheap hack
that lets functions accept multiple types), and checks list to make sure
all types are consistent
"""
if type(file) == str or type(file) == Path:
# put in a list to iterate over
file = [file]

for a_file in file:
if type(a_file) == str:
if not a_file.endswith('.not.mat'):
raise ValueError("all filenames in .not.mat must end with '.not.mat' "
f"but {a_file} does not")
elif type(a_file) == Path:
if not a_file.suffixes == ['.not', '.mat']:
raise ValueError("all filenames in .not.mat must end with '.not.mat' "
f"but {a_file} does not")

return file
from .validation import _parse_file


def notmat2seq(file,
Expand Down Expand Up @@ -76,7 +53,7 @@ def notmat2seq(file,
due to floating point error, e.g. when loading .not.mat files and then sending them to
a csv file, the result should be the same on Windows and Linux
"""
file = _parse_file(file)
file = _parse_file(file, extension='.not.mat')

if abspath and basename:
raise ValueError('abspath and basename arguments cannot both be set to True, '
Expand Down Expand Up @@ -158,7 +135,7 @@ def notmat2csv(file, csv_filename, abspath=False, basename=False):
-------
None
"""
file = _parse_file(file)
file = _parse_file(file, extension='.not.mat')

if abspath and basename:
raise ValueError('abspath and basename arguments cannot both be set to True, '
Expand Down
154 changes: 154 additions & 0 deletions src/crowsetta/textgrid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""module for loading Praat TextGrid files into Sequences
uses the Python library textgrid
https://github.com/kylebgorman/textgrid
a version is distributed with this code (../textgrid) under MIT license
https://github.com/kylebgorman/textgrid/blob/master/LICENSE
"""
import os

import numpy as np
from textgrid import TextGrid, IntervalTier

from .sequence import Sequence
from .meta import Meta
from .csv import seq2csv
from .validation import _parse_file


def textgrid2seq(file,
abspath=False,
basename=False,
round_times=True,
decimals=3,
intervaltier_ind=0,
audio_ext='wav',
):
"""convert Praat Textgrid file into a Sequence
Parameters
----------
file : str, Path
filename of a .TextGrid annotation file, created by Praat.
abspath : bool
if True, converts filename for each audio file into absolute path.
Default is False.
basename : bool
if True, discard any information about path and just use file name.
Default is False.
round_times : bool
if True, round onsets_s and offsets_s.
Default is True.
decimals : int
number of decimals places to round floating point numbers to.
Only meaningful if round_times is True.
Default is 3, so that times are rounded to milliseconds.
intervaltier_ind : int
index of IntervalTier in TextGrid file from which annotations
should be taken. Default it 0, i.e. the first IntervalTier.
Necessary in cases where files have multiple IntervalTiers.
Currently there is only support for converting a single IntervalTier
to a single Sequence.
audio_ext : str
extension of audio file associated with TextGrid file. Used to
determine the filename of the audio file associated with the
TextGrid file. Default is 'wav'.
Returns
-------
seq : crowsetta.Sequence or list of Sequence
each Interval in the first IntervalTier in a TextGrid file
will become one segment in a sequence.
"""
file = _parse_file(file, extension='.TextGrid')
seq = []
for a_textgrid in file:
tg = TextGrid.fromFile(a_textgrid)

intv_tier = tg[intervaltier_ind]
if type(intv_tier) != IntervalTier:
raise ValueError(f'Index specified for IntervalTier was {intervaltier_ind}, '
f'but type at that index was {type(intv_tier)}, not an IntervalTier')

audio_filename = a_textgrid.replace('TextGrid', audio_ext)
if abspath:
audio_filename = os.path.abspath(audio_filename)
elif basename:
audio_filename = os.path.basename(audio_filename)

intv_tier = tg[0]
onsets_s = []
offsets_s = []
labels = []

for interval in intv_tier:
labels.append(interval.mark)
onsets_s.append(interval.minTime)
offsets_s.append(interval.maxTime)

labels = np.asarray(labels)
onsets_s = np.asarray(onsets_s)
offsets_s = np.asarray(offsets_s)

# do this *after* converting onsets_s and offsets_s to onsets_Hz and offsets_Hz
# probably doesn't matter but why introduce more noise?
if round_times:
onsets_s = np.around(onsets_s, decimals=decimals)
offsets_s = np.around(offsets_s, decimals=decimals)

textgrid_seq = Sequence.from_keyword(file=audio_filename,
labels=labels,
onsets_s=onsets_s,
offsets_s=offsets_s)
seq.append(textgrid_seq)

if len(seq) == 1:
return seq[0]
else:
return seq


def textgrid2csv(file, csv_filename, abspath=False, basename=False):
"""saves annotation from TextGrid file(s) in a comma-separated values
(csv) file, where each row represents one syllable from one
.not.mat file.
Parameters
----------
file : str, Path, or list
if list, list of strings or Path objects pointing to TextGrid files
csv_filename : str
name for csv file that is created
The following two parameters specify how file names for audio files are saved. These
options are useful for working with multiple copies of files and for reproducibility.
Default for both is False, in which case the filename is saved just as it is passed to
this function.
abspath : bool
if True, converts filename for each audio file into absolute path.
Default is False.
basename : bool
if True, discard any information about path and just use file name.
Default is False.
Returns
-------
None
"""
file = _parse_file(file, extension='.TextGrid')

if abspath and basename:
raise ValueError('abspath and basename arguments cannot both be set to True, '
'unclear whether absolute path should be saved or if no path '
'information (just base filename) should be saved.')

seq = textgrid2seq(file)
seq2csv(seq, csv_filename, abspath=abspath, basename=basename)


meta = Meta(
name='textgrid',
ext='TextGrid',
to_seq=textgrid2seq,
to_csv=textgrid2csv,
)
33 changes: 24 additions & 9 deletions src/crowsetta/validation.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
"""utilities for input validation.
Adapted from scikit-learn under BSD 3 License
Some utilities adapted from scikit-learn under BSD 3 License
https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/utils/validation.py
Original Authors: Olivier Grisel
Gael Varoquaux
Andreas Mueller
Lars Buitinck
Alexandre Gramfort
Nicolas Tresegnie
"""
import warnings
import numbers
from pathlib import Path

import numpy as np

Expand Down Expand Up @@ -69,3 +62,25 @@ def column_or_row_or_1d(y):
return np.ravel(y)
else:
raise ValueError("bad input shape {0}".format(shape))


def _parse_file(file, extension):
"""helper function that parses/validates value for file argument;
puts a single string or Path into a list to iterate over it (cheap hack
that lets functions accept multiple types), and checks list to make sure
all types are consistent
"""
if type(file) == str or type(file) == Path:
# put in a list to iterate over
file = [file]

for a_file in file:
# cast to string (if it's not already, e.g. it's a Path)
# so we can use .endswith() to compare extensions
# (because using Path.suffixes() would require too much special casing)
a_file = str(a_file)
if not a_file.endswith(extension):
raise ValueError(f"all filenames in 'file' must end with '{extension}' "
f"but {a_file} does not")

return file
5 changes: 5 additions & 0 deletions src/textgrid/AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Max Bane <bane@uchicago.edu>, University of Chicago
Kyle Gorman <kgorman@ling.upenn.edu>, University of Pennsylvania
Morgan Sonderegger <morgan@cs.uchicago.edu>

KG designed this for personal use in 2007, circulated it in 2008, and turned the original methods into Python classes in 2009. MS and MB began fixing bugs and contributing functions for particular use cases in 2011.
7 changes: 7 additions & 0 deletions src/textgrid/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright (c) 2011-2014 Kyle Gorman, Max Bane, Morgan Sonderegger

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 changes: 27 additions & 0 deletions src/textgrid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
textgrid.py
===========

Python classes for Praat TextGrid and TextTier files (and HTK .mlf files)

Kyle Gorman <kylebgorman@gmail.com> and contributors (see commit history).

How to cite:
------------

While you don't have to, if you want to cite textgrid.py in a publication, include a footnote link to the source:

http://github.com/kylebgorman/textgrid.py/

How to install:
---------------

The code can be placed in your working directory or in your `$PYTHONPATH`, and then imported in your Python script. You also can install it via `pip`, like so:

pip install textgrid

(if you're not working in a virtualenv, you may need to do this with `sudo`.)

Synopsis:
---------

See the docstrings in `textgrid.py`
1 change: 1 addition & 0 deletions src/textgrid/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .textgrid import TextGrid, MLF, IntervalTier, PointTier, Interval, Point
3 changes: 3 additions & 0 deletions src/textgrid/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

class TextGridError(Exception):
pass

0 comments on commit 6238925

Please sign in to comment.