#!/usr/bin/env python
# encoding: utf-8
# The MIT License (MIT)
# Copyright (c) 2014-2020 CNRS
# 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.
# AUTHORS
# Hervé BREDIN - http://herve.niderb.fr
# Grant JENKS - http://www.grantjenks.com/
# Paul LERNER
"""
########
Timeline
########
.. plot:: pyplots/timeline.py
:class:`pyannote.core.Timeline` instances are ordered sets of non-empty
segments:
- ordered, because segments are sorted by start time (and end time in case of tie)
- set, because one cannot add twice the same segment
- non-empty, because one cannot add empty segments (*i.e.* start >= end)
There are two ways to define the timeline depicted above:
.. code-block:: ipython
In [25]: from pyannote.core import Timeline, Segment
In [26]: timeline = Timeline()
....: timeline.add(Segment(1, 5))
....: timeline.add(Segment(6, 8))
....: timeline.add(Segment(12, 18))
....: timeline.add(Segment(7, 20))
....:
In [27]: segments = [Segment(1, 5), Segment(6, 8), Segment(12, 18), Segment(7, 20)]
....: timeline = Timeline(segments=segments, uri='my_audio_file') # faster
....:
In [9]: for segment in timeline:
...: print(segment)
...:
[ 00:00:01.000 --> 00:00:05.000]
[ 00:00:06.000 --> 00:00:08.000]
[ 00:00:07.000 --> 00:00:20.000]
[ 00:00:12.000 --> 00:00:18.000]
.. note::
The optional *uri* keyword argument can be used to remember which document it describes.
Several convenient methods are available. Here are a few examples:
.. code-block:: ipython
In [3]: timeline.extent() # extent
Out[3]: <Segment(1, 20)>
In [5]: timeline.support() # support
Out[5]: <Timeline(uri=my_audio_file, segments=[<Segment(1, 5)>, <Segment(6, 20)>])>
In [6]: timeline.duration() # support duration
Out[6]: 18
See :class:`pyannote.core.Timeline` for the complete reference.
"""
import warnings
from typing import (Optional, Iterable, List, Union, Callable,
TextIO, Tuple, TYPE_CHECKING, Iterator, Dict, Text)
from sortedcontainers import SortedList
from . import PYANNOTE_URI, PYANNOTE_SEGMENT
from .json import PYANNOTE_JSON, PYANNOTE_JSON_CONTENT
from .segment import Segment
from .utils.types import Support, Label, CropMode
# this is a moderately ugly way to import `Annotation` to the namespace
# without causing some circular imports :
# https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports
if TYPE_CHECKING:
from .annotation import Annotation
import pandas as pd
# =====================================================================
# Timeline class
# =====================================================================
[docs]class Timeline:
"""
Ordered set of segments.
A timeline can be seen as an ordered set of non-empty segments (Segment).
Segments can overlap -- though adding an already exisiting segment to a
timeline does nothing.
Parameters
----------
segments : Segment iterator, optional
initial set of (non-empty) segments
uri : string, optional
name of segmented resource
Returns
-------
timeline : Timeline
New timeline
"""
@classmethod
def from_df(cls, df: 'pd.DataFrame', uri: Optional[str] = None) -> 'Timeline':
segments = list(df[PYANNOTE_SEGMENT])
timeline = cls(segments=segments, uri=uri)
return timeline
[docs] def __init__(self,
segments: Optional[Iterable[Segment]] = None,
uri: str = None):
if segments is None:
segments = ()
# set of segments (used for checking inclusion)
# Store only non-empty Segments.
segments_set = set([segment for segment in segments if segment])
self.segments_set_ = segments_set
# sorted list of segments (used for sorted iteration)
self.segments_list_ = SortedList(segments_set)
# sorted list of (possibly redundant) segment boundaries
boundaries = (boundary for segment in segments_set for boundary in segment)
self.segments_boundaries_ = SortedList(boundaries)
# path to (or any identifier of) segmented resource
self.uri: str = uri
[docs] def __len__(self):
"""Number of segments
>>> len(timeline) # timeline contains three segments
3
"""
return len(self.segments_set_)
def __nonzero__(self):
return self.__bool__()
[docs] def __bool__(self):
"""Emptiness
>>> if timeline:
... # timeline is not empty
... else:
... # timeline is empty
"""
return len(self.segments_set_) > 0
[docs] def __iter__(self) -> Iterable[Segment]:
"""Iterate over segments (in chronological order)
>>> for segment in timeline:
... # do something with the segment
See also
--------
:class:`pyannote.core.Segment` describes how segments are sorted.
"""
return iter(self.segments_list_)
[docs] def __getitem__(self, k: int) -> Segment:
"""Get segment by index (in chronological order)
>>> first_segment = timeline[0]
>>> penultimate_segment = timeline[-2]
"""
return self.segments_list_[k]
[docs] def __eq__(self, other: 'Timeline'):
"""Equality
Two timelines are equal if and only if their segments are equal.
>>> timeline1 = Timeline([Segment(0, 1), Segment(2, 3)])
>>> timeline2 = Timeline([Segment(2, 3), Segment(0, 1)])
>>> timeline3 = Timeline([Segment(2, 3)])
>>> timeline1 == timeline2
True
>>> timeline1 == timeline3
False
"""
return self.segments_set_ == other.segments_set_
[docs] def __ne__(self, other: 'Timeline'):
"""Inequality"""
return self.segments_set_ != other.segments_set_
[docs] def index(self, segment: Segment) -> int:
"""Get index of (existing) segment
Parameters
----------
segment : Segment
Segment that is being looked for.
Returns
-------
position : int
Index of `segment` in timeline
Raises
------
ValueError if `segment` is not present.
"""
return self.segments_list_.index(segment)
[docs] def add(self, segment: Segment) -> 'Timeline':
"""Add a segment (in place)
Parameters
----------
segment : Segment
Segment that is being added
Returns
-------
self : Timeline
Updated timeline.
Note
----
If the timeline already contains this segment, it will not be added
again, as a timeline is meant to be a **set** of segments (not a list).
If the segment is empty, it will not be added either, as a timeline
only contains non-empty segments.
"""
segments_set_ = self.segments_set_
if segment in segments_set_ or not segment:
return self
segments_set_.add(segment)
self.segments_list_.add(segment)
segments_boundaries_ = self.segments_boundaries_
segments_boundaries_.add(segment.start)
segments_boundaries_.add(segment.end)
return self
[docs] def remove(self, segment: Segment) -> 'Timeline':
"""Remove a segment (in place)
Parameters
----------
segment : Segment
Segment that is being removed
Returns
-------
self : Timeline
Updated timeline.
Note
----
If the timeline does not contain this segment, this does nothing
"""
segments_set_ = self.segments_set_
if segment not in segments_set_:
return self
segments_set_.remove(segment)
self.segments_list_.remove(segment)
segments_boundaries_ = self.segments_boundaries_
segments_boundaries_.remove(segment.start)
segments_boundaries_.remove(segment.end)
return self
[docs] def discard(self, segment: Segment) -> 'Timeline':
"""Same as `remove`
See also
--------
:func:`pyannote.core.Timeline.remove`
"""
return self.remove(segment)
def __ior__(self, timeline: 'Timeline') -> 'Timeline':
return self.update(timeline)
[docs] def update(self, timeline: Segment) -> 'Timeline':
"""Add every segments of an existing timeline (in place)
Parameters
----------
timeline : Timeline
Timeline whose segments are being added
Returns
-------
self : Timeline
Updated timeline
Note
----
Only segments that do not already exist will be added, as a timeline is
meant to be a **set** of segments (not a list).
"""
segments_set = self.segments_set_
segments_set |= timeline.segments_set_
# sorted list of segments (used for sorted iteration)
self.segments_list_ = SortedList(segments_set)
# sorted list of (possibly redundant) segment boundaries
boundaries = (boundary for segment in segments_set for boundary in segment)
self.segments_boundaries_ = SortedList(boundaries)
return self
def __or__(self, timeline: 'Timeline') -> 'Timeline':
return self.union(timeline)
[docs] def union(self, timeline: 'Timeline') -> 'Timeline':
"""Create new timeline made of union of segments
Parameters
----------
timeline : Timeline
Timeline whose segments are being added
Returns
-------
union : Timeline
New timeline containing the union of both timelines.
Note
----
This does the same as timeline.update(...) except it returns a new
timeline, and the original one is not modified.
"""
segments = self.segments_set_ | timeline.segments_set_
return Timeline(segments=segments, uri=self.uri)
[docs] def co_iter(self, other: 'Timeline') -> Iterator[Tuple[Segment, Segment]]:
"""Iterate over pairs of intersecting segments
>>> timeline1 = Timeline([Segment(0, 2), Segment(1, 2), Segment(3, 4)])
>>> timeline2 = Timeline([Segment(1, 3), Segment(3, 5)])
>>> for segment1, segment2 in timeline1.co_iter(timeline2):
... print(segment1, segment2)
(<Segment(0, 2)>, <Segment(1, 3)>)
(<Segment(1, 2)>, <Segment(1, 3)>)
(<Segment(3, 4)>, <Segment(3, 5)>)
Parameters
----------
other : Timeline
Second timeline
Returns
-------
iterable : (Segment, Segment) iterable
Yields pairs of intersecting segments in chronological order.
"""
for segment in self.segments_list_:
# iterate over segments that starts before 'segment' ends
temp = Segment(start=segment.end, end=segment.end)
for other_segment in other.segments_list_.irange(maximum=temp):
if segment.intersects(other_segment):
yield segment, other_segment
[docs] def crop_iter(self,
support: Support,
mode: CropMode = 'intersection',
returns_mapping: bool = False) \
-> Iterator[Union[Tuple[Segment, Segment], Segment]]:
"""Like `crop` but returns a segment iterator instead
See also
--------
:func:`pyannote.core.Timeline.crop`
"""
if mode not in {'loose', 'strict', 'intersection'}:
raise ValueError("Mode must be one of 'loose', 'strict', or "
"'intersection'.")
if not isinstance(support, (Segment, Timeline)):
raise TypeError("Support must be a Segment or a Timeline.")
if isinstance(support, Segment):
# corner case where "support" is empty
if support:
segments = [support]
else:
segments = []
support = Timeline(segments=segments, uri=self.uri)
for yielded in self.crop_iter(support, mode=mode,
returns_mapping=returns_mapping):
yield yielded
return
# if 'support' is a `Timeline`, we use its support
support = support.support()
# loose mode
if mode == 'loose':
for segment, _ in self.co_iter(support):
yield segment
return
# strict mode
if mode == 'strict':
for segment, other_segment in self.co_iter(support):
if segment in other_segment:
yield segment
return
# intersection mode
for segment, other_segment in self.co_iter(support):
mapped_to = segment & other_segment
if not mapped_to:
continue
if returns_mapping:
yield segment, mapped_to
else:
yield mapped_to
[docs] def crop(self,
support: Support,
mode: CropMode = 'intersection',
returns_mapping: bool = False) \
-> Union['Timeline', Tuple['Timeline', Dict[Segment, Segment]]]:
"""Crop timeline to new support
Parameters
----------
support : Segment or Timeline
If `support` is a `Timeline`, its support is used.
mode : {'strict', 'loose', 'intersection'}, optional
Controls how segments that are not fully included in `support` are
handled. 'strict' mode only keeps fully included segments. 'loose'
mode keeps any intersecting segment. 'intersection' mode keeps any
intersecting segment but replace them by their actual intersection.
returns_mapping : bool, optional
In 'intersection' mode, return a dictionary whose keys are segments
of the cropped timeline, and values are list of the original
segments that were cropped. Defaults to False.
Returns
-------
cropped : Timeline
Cropped timeline
mapping : dict
When 'returns_mapping' is True, dictionary whose keys are segments
of 'cropped', and values are lists of corresponding original
segments.
Examples
--------
>>> timeline = Timeline([Segment(0, 2), Segment(1, 2), Segment(3, 4)])
>>> timeline.crop(Segment(1, 3))
<Timeline(uri=None, segments=[<Segment(1, 2)>])>
>>> timeline.crop(Segment(1, 3), mode='loose')
<Timeline(uri=None, segments=[<Segment(0, 2)>, <Segment(1, 2)>])>
>>> timeline.crop(Segment(1, 3), mode='strict')
<Timeline(uri=None, segments=[<Segment(1, 2)>])>
>>> cropped, mapping = timeline.crop(Segment(1, 3), returns_mapping=True)
>>> print(mapping)
{<Segment(1, 2)>: [<Segment(0, 2)>, <Segment(1, 2)>]}
"""
if mode == 'intersection' and returns_mapping:
segments, mapping = [], {}
for segment, mapped_to in self.crop_iter(support,
mode='intersection',
returns_mapping=True):
segments.append(mapped_to)
mapping[mapped_to] = mapping.get(mapped_to, list()) + [segment]
return Timeline(segments=segments, uri=self.uri), mapping
return Timeline(segments=self.crop_iter(support, mode=mode),
uri=self.uri)
[docs] def overlapping(self, t: float) -> List[Segment]:
"""Get list of segments overlapping `t`
Parameters
----------
t : float
Timestamp, in seconds.
Returns
-------
segments : list
List of all segments of timeline containing time t
"""
return list(self.overlapping_iter(t))
[docs] def overlapping_iter(self, t: float) -> Iterator[Segment]:
"""Like `overlapping` but returns a segment iterator instead
See also
--------
:func:`pyannote.core.Timeline.overlapping`
"""
segment = Segment(start=t, end=t)
for segment in self.segments_list_.irange(maximum=segment):
if segment.overlaps(t):
yield segment
[docs] def get_overlap(self) -> 'Timeline':
"""Get overlapping parts of the timeline.
A simple illustration:
timeline
|------| |------| |----|
|--| |-----| |----------|
timeline.get_overlap()
|--| |---| |----|
Returns
-------
overlap : `pyannote.core.Timeline`
Timeline of the overlaps.
"""
overlaps_tl = Timeline(uri=self.uri)
for s1, s2 in self.co_iter(self):
if s1 == s2:
continue
overlaps_tl.add(s1 & s2)
return overlaps_tl.support()
[docs] def extrude(self,
removed: Support,
mode: CropMode = 'intersection') -> 'Timeline':
"""Remove segments that overlap `removed` support.
Parameters
----------
removed : Segment or Timeline
If `support` is a `Timeline`, its support is used.
mode : {'strict', 'loose', 'intersection'}, optional
Controls how segments that are not fully included in `removed` are
handled. 'strict' mode only removes fully included segments. 'loose'
mode removes any intersecting segment. 'intersection' mode removes
the overlapping part of any intersecting segment.
Returns
-------
extruded : Timeline
Extruded timeline
Examples
--------
>>> timeline = Timeline([Segment(0, 2), Segment(1, 2), Segment(3, 5)])
>>> timeline.extrude(Segment(1, 2))
<Timeline(uri=None, segments=[<Segment(0, 1)>, <Segment(3, 5)>])>
>>> timeline.extrude(Segment(1, 3), mode='loose')
<Timeline(uri=None, segments=[<Segment(3, 5)>])>
>>> timeline.extrude(Segment(1, 3), mode='strict')
<Timeline(uri=None, segments=[<Segment(0, 2)>, <Segment(3, 5)>])>
"""
if isinstance(removed, Segment):
removed = Timeline([removed])
extent_tl = Timeline([self.extent()], uri=self.uri)
truncating_support = removed.gaps(support=extent_tl)
# loose for truncate means strict for crop and vice-versa
if mode == "loose":
mode = "strict"
elif mode == "strict":
mode = "loose"
return self.crop(truncating_support, mode=mode)
[docs] def __str__(self):
"""Human-readable representation
>>> timeline = Timeline(segments=[Segment(0, 10), Segment(1, 13.37)])
>>> print(timeline)
[[ 00:00:00.000 --> 00:00:10.000]
[ 00:00:01.000 --> 00:00:13.370]]
"""
n = len(self.segments_list_)
string = "["
for i, segment in enumerate(self.segments_list_):
string += str(segment)
string += "\n " if i + 1 < n else ""
string += "]"
return string
[docs] def __repr__(self):
"""Computer-readable representation
>>> Timeline(segments=[Segment(0, 10), Segment(1, 13.37)])
<Timeline(uri=None, segments=[<Segment(0, 10)>, <Segment(1, 13.37)>])>
"""
return "<Timeline(uri=%s, segments=%s)>" % (self.uri,
list(self.segments_list_))
[docs] def __contains__(self, included: Union[Segment, 'Timeline']):
"""Inclusion
Check whether every segment of `included` does exist in timeline.
Parameters
----------
included : Segment or Timeline
Segment or timeline being checked for inclusion
Returns
-------
contains : bool
True if every segment in `included` exists in timeline,
False otherwise
Examples
--------
>>> timeline1 = Timeline(segments=[Segment(0, 10), Segment(1, 13.37)])
>>> timeline2 = Timeline(segments=[Segment(0, 10)])
>>> timeline1 in timeline2
False
>>> timeline2 in timeline1
>>> Segment(1, 13.37) in timeline1
True
"""
if isinstance(included, Segment):
return included in self.segments_set_
elif isinstance(included, Timeline):
return self.segments_set_.issuperset(included.segments_set_)
else:
raise TypeError(
'Checking for inclusion only supports Segment and '
'Timeline instances')
[docs] def empty(self) -> 'Timeline':
"""Return an empty copy
Returns
-------
empty : Timeline
Empty timeline using the same 'uri' attribute.
"""
return Timeline(uri=self.uri)
[docs] def covers(self, other: 'Timeline') -> bool:
"""Check whether other timeline is fully covered by the timeline
Parameter
---------
other : Timeline
Second timeline
Returns
-------
covers : bool
True if timeline covers "other" timeline entirely. False if at least
one segment of "other" is not fully covered by timeline
"""
# compute gaps within "other" extent
# this is where we should look for possible faulty segments
gaps = self.gaps(support=other.extent())
# if at least one gap intersects with a segment from "other",
# "self" does not cover "other" entirely --> return False
for _ in gaps.co_iter(other):
return False
# if no gap intersects with a segment from "other",
# "self" covers "other" entirely --> return True
return True
[docs] def copy(self, segment_func: Optional[Callable[[Segment], Segment]] = None) \
-> 'Timeline':
"""Get a copy of the timeline
If `segment_func` is provided, it is applied to each segment first.
Parameters
----------
segment_func : callable, optional
Callable that takes a segment as input, and returns a segment.
Defaults to identity function (segment_func(segment) = segment)
Returns
-------
timeline : Timeline
Copy of the timeline
"""
# if segment_func is not provided
# just add every segment
if segment_func is None:
return Timeline(segments=self.segments_list_, uri=self.uri)
# if is provided
# apply it to each segment before adding them
return Timeline(segments=[segment_func(s) for s in self.segments_list_],
uri=self.uri)
[docs] def extent(self) -> Segment:
"""Extent
The extent of a timeline is the segment of minimum duration that
contains every segments of the timeline. It is unique, by definition.
The extent of an empty timeline is an empty segment.
A picture is worth a thousand words::
timeline
|------| |------| |----|
|--| |-----| |----------|
timeline.extent()
|--------------------------------|
Returns
-------
extent : Segment
Timeline extent
Examples
--------
>>> timeline = Timeline(segments=[Segment(0, 1), Segment(9, 10)])
>>> timeline.extent()
<Segment(0, 10)>
"""
if self.segments_set_:
segments_boundaries_ = self.segments_boundaries_
start = segments_boundaries_[0]
end = segments_boundaries_[-1]
return Segment(start=start, end=end)
return Segment(start=0.0, end=0.0)
[docs] def support_iter(self, collar: float = 0.0) -> Iterator[Segment]:
"""Like `support` but returns a segment generator instead
See also
--------
:func:`pyannote.core.Timeline.support`
"""
# The support of an empty timeline is an empty timeline.
if not self:
return
# Principle:
# * gather all segments with no gap between them
# * add one segment per resulting group (their union |)
# Note:
# Since segments are kept sorted internally,
# there is no need to perform an exhaustive segment clustering.
# We just have to consider them in their natural order.
# Initialize new support segment
# as very first segment of the timeline
new_segment = self.segments_list_[0]
for segment in self:
# If there is no gap between new support segment and next segment
# OR there is a gap with duration < collar seconds,
possible_gap = segment ^ new_segment
if not possible_gap or possible_gap.duration < collar:
# Extend new support segment using next segment
new_segment |= segment
# If there actually is a gap and the gap duration >= collar
# seconds,
else:
yield new_segment
# Initialize new support segment as next segment
# (right after the gap)
new_segment = segment
# Add new segment to the timeline support
yield new_segment
[docs] def support(self, collar: float = 0.) -> 'Timeline':
"""Timeline support
The support of a timeline is the timeline with the minimum number of
segments with exactly the same time span as the original timeline. It
is (by definition) unique and does not contain any overlapping
segments.
A picture is worth a thousand words::
collar
|---|
timeline
|------| |------| |----|
|--| |-----| |----------|
timeline.support()
|------| |--------| |----------|
timeline.support(collar)
|------------------| |----------|
Parameters
----------
collar : float, optional
Merge separated by less than `collar` seconds. This is why there
are only two segments in the final timeline in the above figure.
Defaults to 0.
Returns
-------
support : Timeline
Timeline support
"""
return Timeline(segments=self.support_iter(collar), uri=self.uri)
[docs] def duration(self) -> float:
"""Timeline duration
The timeline duration is the sum of the durations of the segments
in the timeline support.
Returns
-------
duration : float
Duration of timeline support, in seconds.
"""
# The timeline duration is the sum of the durations
# of the segments in the timeline support.
return sum(s.duration for s in self.support_iter())
[docs] def gaps_iter(self, support: Optional[Support] = None) -> Iterator[Segment]:
"""Like `gaps` but returns a segment generator instead
See also
--------
:func:`pyannote.core.Timeline.gaps`
"""
if support is None:
support = self.extent()
if not isinstance(support, (Segment, Timeline)):
raise TypeError("unsupported operand type(s) for -':"
"%s and Timeline." % type(support).__name__)
# segment support
if isinstance(support, Segment):
# `end` is meant to store the end time of former segment
# initialize it with beginning of provided segment `support`
end = support.start
# support on the intersection of timeline and provided segment
for segment in self.crop(support, mode='intersection').support():
# add gap between each pair of consecutive segments
# if there is no gap, segment is empty, therefore not added
gap = Segment(start=end, end=segment.start)
if gap:
yield gap
# keep track of the end of former segment
end = segment.end
# add final gap (if not empty)
gap = Segment(start=end, end=support.end)
if gap:
yield gap
# timeline support
elif isinstance(support, Timeline):
# yield gaps for every segment in support of provided timeline
for segment in support.support():
for gap in self.gaps_iter(support=segment):
yield gap
[docs] def gaps(self, support: Optional[Support] = None) \
-> 'Timeline':
"""Gaps
A picture is worth a thousand words::
timeline
|------| |------| |----|
|--| |-----| |----------|
timeline.gaps()
|--| |--|
Parameters
----------
support : None, Segment or Timeline
Support in which gaps are looked for. Defaults to timeline extent
Returns
-------
gaps : Timeline
Timeline made of all gaps from original timeline, and delimited
by provided support
See also
--------
:func:`pyannote.core.Timeline.extent`
"""
return Timeline(segments=self.gaps_iter(support=support),
uri=self.uri)
[docs] def segmentation(self) -> 'Timeline':
"""Segmentation
Create the unique timeline with same support and same set of segment
boundaries as original timeline, but with no overlapping segments.
A picture is worth a thousand words::
timeline
|------| |------| |----|
|--| |-----| |----------|
timeline.segmentation()
|-|--|-| |-|---|--| |--|----|--|
Returns
-------
timeline : Timeline
(unique) timeline with same support and same set of segment
boundaries as original timeline, but with no overlapping segments.
"""
# COMPLEXITY: O(n)
support = self.support()
# COMPLEXITY: O(n.log n)
# get all boundaries (sorted)
# |------| |------| |----|
# |--| |-----| |----------|
# becomes
# | | | | | | | | | | | |
timestamps = set([])
for (start, end) in self:
timestamps.add(start)
timestamps.add(end)
timestamps = sorted(timestamps)
# create new partition timeline
# | | | | | | | | | | | |
# becomes
# |-|--|-| |-|---|--| |--|----|--|
# start with an empty copy
timeline = Timeline(uri=self.uri)
if len(timestamps) == 0:
return Timeline(uri=self.uri)
segments = []
start = timestamps[0]
for end in timestamps[1:]:
# only add segments that are covered by original timeline
segment = Segment(start=start, end=end)
if segment and support.overlapping(segment.middle):
segments.append(segment)
# next segment...
start = end
return Timeline(segments=segments, uri=self.uri)
[docs] def to_annotation(self,
generator: Union[str, Iterable[Label], None, None] = 'string',
modality: Optional[str] = None) \
-> 'Annotation':
"""Turn timeline into an annotation
Each segment is labeled by a unique label.
Parameters
----------
generator : 'string', 'int', or iterable, optional
If 'string' (default) generate string labels. If 'int', generate
integer labels. If iterable, use it to generate labels.
modality : str, optional
Returns
-------
annotation : Annotation
Annotation
"""
from .annotation import Annotation
annotation = Annotation(uri=self.uri, modality=modality)
if generator == 'string':
from .utils.generators import string_generator
generator = string_generator()
elif generator == 'int':
from .utils.generators import int_generator
generator = int_generator()
for segment in self:
annotation[segment] = next(generator)
return annotation
[docs] def write_uem(self, file: TextIO):
"""Dump timeline to file using UEM format
Parameters
----------
file : file object
Usage
-----
>>> with open('file.uem', 'w') as file:
... timeline.write_uem(file)
"""
uri = self.uri if self.uri else "<NA>"
if isinstance(uri, Text) and ' ' in uri:
msg = (f'Space-separated UEM file format does not allow file URIs '
f'containing spaces (got: "{uri}").')
raise ValueError(msg)
for segment in self:
line = f"{uri} 1 {segment.start:.3f} {segment.end:.3f}\n"
file.write(line)
[docs] def for_json(self):
"""Serialization
See also
--------
:mod:`pyannote.core.json`
"""
data = {PYANNOTE_JSON: self.__class__.__name__}
data[PYANNOTE_JSON_CONTENT] = [s.for_json() for s in self]
if self.uri:
data[PYANNOTE_URI] = self.uri
return data
[docs] @classmethod
def from_json(cls, data):
"""Deserialization
See also
--------
:mod:`pyannote.core.json`
"""
uri = data.get(PYANNOTE_URI, None)
segments = [Segment.from_json(s) for s in data[PYANNOTE_JSON_CONTENT]]
return cls(segments=segments, uri=uri)
def _repr_png_(self):
"""IPython notebook support
See also
--------
:mod:`pyannote.core.notebook`
"""
from .notebook import MATPLOTLIB_IS_AVAILABLE, MATPLOTLIB_WARNING
if not MATPLOTLIB_IS_AVAILABLE:
warnings.warn(MATPLOTLIB_WARNING.format(klass=self.__class__.__name__))
return None
from .notebook import repr_timeline
return repr_timeline(self)