import copy
from functools import total_ordering
[docs]@total_ordering
class Label:
"""
Represents a label that describes some part of an utterance.
Args:
value (str): The text of the label.
start (float): Start of the label within the utterance in seconds.
(default: 0)
end (float): End of the label within the utterance in seconds.
(default: inf) (inf defines the end of the utterance)
meta (dict): A dictionary containing additional information
for the label.
Attributes:
label_list (LabelList): The label-list this label is belonging to.
"""
__slots__ = ['value', 'start', 'end', 'label_list', 'meta']
def __init__(self, value, start=0, end=float('inf'), meta=None):
self.value = value
self.start = start
self.end = end
self.meta = meta or {}
self.label_list = None
def __eq__(self, other):
return (
self.start == other.start and
self.end == other.end and
self.value.lower() == other.value.lower()
)
def __lt__(self, other):
data_this = (self.start, self.end, self.value.lower())
data_other = (other.start, other.end, other.value.lower())
return data_this < data_other
def __repr__(self) -> str:
return 'Label({}, {}, {})'.format(self.value, self.start, self.end)
def __copy__(self):
return Label(
self.value,
start=self.start,
end=self.end,
meta=self.meta
)
def __deepcopy__(self, memo):
return Label(
self.value,
start=self.start,
end=self.end,
meta=copy.deepcopy(self.meta, memo)
)
@property
def start_abs(self):
"""
Return the absolute start of the label in seconds relative to
the signal. If the label isn't linked to any utterance via label-list,
it is assumed ``self.start`` is relative to the start of the signal,
hence ``self.start`` == ``self.start_abs``.
"""
if self.label_list is None or self.label_list.utterance is None:
return self.start
return self.label_list.utterance.start + self.start
@property
def end_abs(self):
"""
Return the absolute end of the label in seconds relative to the signal.
If the label isn't linked to any utterance via label-list,
it is assumed ``self.end`` is relative to the start of the signal,
hence ``self.end`` == ``self.end_abs``.
"""
if self.label_list is None or self.label_list.utterance is None:
return self.end
elif self.end == float('inf'):
return self.label_list.utterance.end_abs
else:
return self.end + self.label_list.utterance.start
@property
def duration(self):
""" Return the duration of the label in seconds. """
return self.end_abs - self.start_abs
@property
def length(self):
""" Return the length of the label (Number of characters). """
return len(self.value)
[docs] def read_samples(self, sr=None):
"""
Read the samples of the utterance.
Args:
sr (int): If None uses the sampling rate given by the track,
otherwise resamples to the given sampling rate.
Returns:
np.ndarray: A numpy array containing the samples as a
floating point (numpy.float32) time series.
"""
duration = None
if self.end != float('inf') or self.label_list.utterance.end >= 0:
duration = self.duration
track = self.label_list.utterance.track
return track.read_samples(
sr=sr,
offset=self.start_abs,
duration=duration
)
[docs] def tokenized(self, delimiter=' '):
"""
Return a list with tokens from the value of the label.
Tokens are extracted by splitting the string using ``delimiter`` and
then trimming any whitespace before and after splitted strings.
Args:
delimiter (str): The delimiter used to split into tokens.
(default: space)
Return:
list: A list of tokens in the order they occur in the label.
Examples:
>>> label = Label('as is oh')
>>> label.tokenized()
['as', 'is', 'oh']
Using a different delimiter (whitespace is trimmed anyway):
>>> label = Label('oh hi, as, is ')
>>> label.tokenized(delimiter=',')
['oh hi', 'as', 'is']
"""
tokens = self.value.split(sep=delimiter)
tokens = [t.strip() for t in tokens]
while '' in tokens:
tokens.remove('')
return tokens
[docs] def do_overlap(self, other_label, adjacent=True):
"""
Determine whether ``other_label`` overlaps with this label.
If ``adjacent==True``, adjacent labels are also considered as overlapping.
Args:
other_label (Label): Another label.
adjacent (bool): If ``True``, adjacent labels are
considered as overlapping.
Returns:
bool: ``True`` if the two labels overlap, ``False`` otherwise.
"""
this_end = self.end
other_end = other_label.end
if this_end == float('inf'):
this_end = self.end_abs
if other_end == float('inf'):
other_end = other_label.end_abs
if adjacent:
first_ends_before = this_end < other_label.start
second_ends_before = other_end < self.start
else:
first_ends_before = this_end <= other_label.start
second_ends_before = other_end <= self.start
return not (first_ends_before or second_ends_before)
[docs] def overlap_duration(self, other_label):
"""
Return the duration of the overlapping part between this label
and ``other_label``.
Args:
other_label(Label): Another label to check.
Return:
float: The duration of overlap in seconds.
Example:
>>> label_a = Label('a', 3.4, 5.6)
>>> label_b = Label('b', 4.8, 6.2)
>>> label_a.overlap_duration(label_b)
0.8
"""
this_end = self.end
other_end = other_label.end
if this_end == float('inf'):
this_end = self.end_abs
if other_end == float('inf'):
other_end = other_label.end_abs
start_overlap = max(self.start, other_label.start)
end_overlap = min(this_end, other_end)
return max(0, end_overlap - start_overlap)