# Copyright (c) 2023, Trustees of the University of Pennsylvania
# See LICENSE for licensing conditions
"""Labeled segments."""
from dataclasses import dataclass
import math
from typing import Iterable, List
from .utils import add_dataclass_slots, clip
__all__ = ['Segment']
# Implementation inspired by pyannote.core.segment.
[docs]@add_dataclass_slots
@dataclass(unsafe_hash=True, order=True)
class Segment:
"""Speech segment.
Parameters
----------
onset : float
Onset of segment in seconds from beginning of recording.
offset : float
Offset of segment in seconds from beginning of recording.
"""
onset: float
offset: float
[docs] def gap(self, other):
"""Return gap between segment and another segment.
If the two segments overlap, the gap will have duration <= 0.
Parameters
----------
other : Segment
Other segment.
Returns
-------
Segment
Gap segment.
"""
onset = min(self.offset, other.offset)
offset = max(self.onset, other.onset)
return Segment(onset, offset)
[docs] def union(self, *other):
"""Return union of segments.
The union of a set of segments is defined as the minimal segment
containing each segment in the set.
Parameters
----------
other : Segment
Other segment.
Returns
-------
Segment
Union of the segments.
"""
segs = [self]
segs.extend(other)
onset = min(s.onset for s in segs)
offset = max(s.offset for s in segs)
return Segment(onset, offset)
[docs] def copy(self):
"""Return deep copy of segment."""
return Segment(onset=self.onset, offset=self.offset)
[docs] def shift(self, delta, in_place=False):
"""Shift segment by `delta` seconds."""
if not in_place:
self = self.copy()
self.onset += delta
self.offset += delta
return self
[docs] def clip(self, lb, ub, in_place=False):
"""Clip segment so that its onset/offset lay within [`lb`, `ub`].
Parameters
----------
lb : float
Lowerbound of interval.
ub : float
Upperbound of interval.
in_place : bool, optional
If True, perform operation in place.
Returns
-------
Segment
Clipped segment.
"""
if not in_place:
self = self.copy()
self.onset = clip(self.onset, lb, ub)
self.offset = clip(self.offset, lb, ub)
return self
[docs] def round(self, precision=3, in_place=False):
"""Round onset/offset to `precision` digits."""
if not in_place:
self = self.copy()
self.onset = round(self.onset, precision)
self.offset = round(self.offset, precision)
return self
[docs] def isclose(self, other, atol=1e-7):
"""Return True if onsets/offsets of segments are equal within a
tolerance.
Parameters
----------
other : Segment
Segment to compare with.
atol : float, optional
Times within `atol` seconds are considered close.
(Default: 1e-7)
"""
return (math.isclose(self.onset, other.onset, abs_tol=atol) and
math.isclose(self.offset, other.offset, abs_tol=atol))
[docs] @staticmethod
def allclose(lsegs, rsegs, atol=1e-7):
"""Return True if two lists of segments are element-wise equal within a
tolerance.
Two segments are considered equal if their onsets/offsets are within
`atol` of each other.
Parameters
----------
lsegs, rsegs : Iterable[Segment]
Lists of segments to compare.
atol : float, optional
Times within `atol` seconds are considered close.
(Default: 1e-7)
"""
lsegs = list(lsegs)
rsegs = list(rsegs)
if not len(lsegs) == len(rsegs):
return False
for lseg, rseg in zip(lsegs, rsegs):
if not lseg.isclose(rseg, atol=atol):
return False
return True
[docs] @staticmethod
def merge_segs(segs, thresh=0.0, is_sorted=False, copy=True):
"""Merge segments.
Produces a new segmentation from `segs` by:
- merging overlapping segments
- merging segments separated by <= `thresh` seconds.
Parameters
----------
segs : Iterable[Segment]
Segments to be merged.
thresh : float, optional
Tolerance for merging. Segments separated by <= `thresh` seconds
will be merged.
(Default: 0.0)
is_sorted : bool, optional
If True, treat `segs` as already sorted. Otherwise, sort before
performing mergers.
(Default: False)
copy : bool, optional
If True, create copy of `segs` and perform merger on this copy.
(Default: True)
Returns
-------
List[Segment]
Merged segments.
"""
if not segs:
return []
if copy:
segs = [seg.copy() for seg in segs]
if not is_sorted:
segs = sorted(segs)
# Perform merger.
merged_segs = []
curr_seg = segs[0]
for seg in segs:
gap = curr_seg ^ seg
if gap.duration > thresh:
merged_segs.append(curr_seg)
curr_seg = seg
curr_seg = curr_seg | seg
merged_segs.append(curr_seg)
return merged_segs
@property
def duration(self):
"""Segment duration in seconds."""
return self.offset - self.onset
def __iter__(self):
"""Unpack segment for easy interoperability with tuples.
>>> seg = Segment(0.1, 0.5)
>>> onset, offset = seg
"""
yield self.onset
yield self.offset
def __bool__(self):
return self.duration > 0
def __or__(self, other):
return self.union(other)
def __xor__(self, other):
return self.gap(other)
def __ne__(self, other):
return not self.__eq__(other)