class Timeline(object): """ 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 segments uri : string, optional name of segmented resource Returns ------- timeline : Timeline New timeline Examples -------- Create a new empty timeline >>> timeline = Timeline() >>> if not timeline: ... print "Timeline is empty." Timeline is empty. Add one segment (+=) >>> segment = Segment(0, 1) >>> timeline.add(segment) >>> if len(timeline) == 1: ... print "Timeline contains only one segment." Timeline contains only one segment. Add all segments from another timeline >>> other_timeline = Timeline([Segment(0.5, 3), Segment(6, 8)]) >>> timeline.update(other_timeline) Get timeline extent, coverage & duration >>> extent = timeline.extent() >>> print extent [0 --> 8] >>> coverage = timeline.coverage() >>> print coverage [ [0 --> 3] [6 --> 8] ] >>> duration = timeline.duration() >>> print "Timeline covers a total of %g seconds." % duration Timeline covers a total of 5 seconds. Iterate over (sorted) timeline segments >>> for segment in timeline: ... print segment [0 --> 1] [0.5 --> 3] [6 --> 8] Segmentation >>> segmentation = timeline.segmentation() >>> print segmentation [ [0 --> 0.5] [0.5 --> 1] [1 --> 3] [6 --> 8] ] Gaps >>> timeline = timeline.copy() >>> print timeline [ [0 --> 1] [0.5 --> 3] [6 --> 8] ] >>> print timeline.gaps() [ [3 --> 6] ] >>> segment = Segment(0, 10) >>> print timeline.gaps(segment) [ [3 --> 6] [8 --> 10] ] """ @classmethod def from_df(cls, df, uri=None): segments = list(df[PYANNOTE_SEGMENT]) timeline = cls(segments=segments, uri=uri) return timeline def __init__(self, segments=None, uri=None): super(Timeline, self).__init__() # sorted set of segments (as an augmented red-black tree) segments = [s for s in segments if s] if segments else [] self._segments = SortedSet(items=segments, key_type=(float, float), updator=TimelineUpdator) # path to (or any identifier of) segmented resource self.uri = uri def __len__(self): return self._segments.length() def __nonzero__(self): return self._segments.length() > 0 def __iter__(self): return iter(self._segments) def __getitem__(self, k): """Returns kth segment""" return self._segments.kth(k) def __eq__(self, other): return self._segments == other._segments def __ne__(self, other): return self._segments != other._segments def index(self, segment): """Index of segment Parameter --------- segment : Segment Raises ------ ValueError if the segment is not present """ return self._segments.index(segment) def add(self, segment): """Add segment""" if segment: self._segments.add(segment) def update(self, timeline): """Add `timeline` segments""" self._segments.update(timeline._segments) def union(self, other): """Create new timeline made of union of segments""" segments = self._segments.union(other._segments) return Timeline(segments=segments, uri=self.uri) def co_iter(self, other): for segment, other_segment in self._segments.co_iter(other._segments): yield segment, other_segment def crop(self, other, mode='intersection', mapping=False): if isinstance(other, Segment): other = Timeline(segments=[other], uri=self.uri) return self.crop(other, mode=mode, mapping=mapping) elif isinstance(other, Timeline): if mode == 'loose': segments = [segment for segment, _ in self.co_iter(other)] return Timeline(segments=segments, uri=self.uri) elif mode == 'strict': segments = [segment for segment, other_segment in self.co_iter(other) if segment in other_segment] return Timeline(segments=segments, uri=self.uri) elif mode == 'intersection': if mapping: mapping = {} for segment, other_segment in self.co_iter(other): inter = segment & other_segment mapping[inter] = mapping.get(inter, list()) + [segment] return Timeline(segments=mapping, uri=self.uri), mapping else: segments = [segment & other_segment for segment, other_segment in self.co_iter(other)] return Timeline(segments=segments, uri=self.uri) else: raise NotImplementedError("unsupported mode: '%s'" % mode) def overlapping(self, timestamp): """Get list of segments overlapping `timestamp`""" return self._segments.overlapping(timestamp) def __str__(self): """Human-friendly representation""" string = "[\n" for segment in self._segments: string += " %s\n" % str(segment) string += "]" return string def __repr__(self): return "<Timeline(uri=%s, segments=%s)>" % (self.uri, list(self._segments)) def __contains__(self, included): """Inclusion Use expression 'segment in timeline' or 'other_timeline in timeline' Parameters ---------- included : `Segment` or `Timeline` Returns ------- contains : bool True if every segment in `included` exists in timeline, False otherwise """ if isinstance(included, Segment): return included in self._segments elif isinstance(included, Timeline): return self._segments.issuperset(included._segments) else: raise TypeError() def empty(self): """Empty copy of a timeline. Examples -------- >>> timeline = Timeline(uri="MyVideo.avi") >>> timeline += [Segment(0, 1), Segment(2, 3)] >>> empty = timeline.empty() >>> print empty.uri MyVideo.avi >>> print empty [ ] """ return Timeline(uri=self.uri) def copy(self, segment_func=None): """Duplicate timeline. If segment_func is provided, apply it to each segment first. Parameters ---------- segment_func : function Returns ------- timeline : Timeline A (possibly modified) copy of the timeline Examples -------- >>> timeline = Timeline(uri="MyVideo.avi") >>> timeline += [Segment(0, 1), Segment(2, 3)] >>> cp = timeline.copy() >>> print cp.uri MyVideo.avi >>> print cp [ [0 --> 1] [2 --> 3] ] """ # if segment_func is not provided # just add every segment if segment_func is None: return Timeline(segments=self._segments, uri=self.uri) # if is provided # apply it to each segment before adding them else: return Timeline(segments=[segment_func(s) for s in self._segments], uri=self.uri) def extent(self): """Timeline 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. Returns ------- extent : Segment Timeline extent Examples -------- >>> timeline = Timeline(uri="MyVideo.avi") >>> timeline += [Segment(0, 1), Segment(9, 10)] >>> print timeline.extent() [0 --> 10] """ return self._segments.extent() def coverage(self): """Timeline coverage The coverage of 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. Returns ------- coverage : Timeline Timeline coverage """ # make sure URI attribute is kept. coverage = Timeline(uri=self.uri) # The coverage of an empty timeline is an empty timeline. if not self: return coverage # 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 coverage segment # as very first segment of the timeline new_segment = self._segments.kth(0) for segment in self: # If there is no gap between new coverage segment and next segment, if not (segment ^ new_segment): # Extend new coverage segment using next segment new_segment |= segment # If there actually is a gap, else: # Add new segment to the timeline coverage coverage.add(new_segment) # Initialize new coverage segment as next segment # (right after the gap) new_segment = segment # Add new segment to the timeline coverage coverage.add(new_segment) return coverage def duration(self): """Timeline duration Returns ------- duration : float Duration of timeline coverage, in seconds. """ # The timeline duration is the sum of the durations # of the segments in the timeline coverage. return sum([s.duration for s in self.coverage()]) def gaps(self, focus=None): """Timeline gaps Parameters ---------- focus : None, Segment or Timeline Returns ------- gaps : Timeline Timeline made of all gaps from original timeline, and delimited by provided segment or timeline. Raises ------ TypeError when `focus` is neither None, Segment nor Timeline Examples -------- """ if focus is None: focus = self.extent() if not isinstance(focus, (Segment, Timeline)): raise TypeError("unsupported operand type(s) for -':" "%s and Timeline." % type(focus).__name__) # segment focus if isinstance(focus, Segment): # starts with an empty timeline timeline = self.empty() # `end` is meant to store the end time of former segment # initialize it with beginning of provided segment `focus` end = focus.start # focus on the intersection of timeline and provided segment for segment in self.crop(focus, mode='intersection').coverage(): # add gap between each pair of consecutive segments # if there is no gap, segment is empty, therefore not added timeline.add(Segment(start=end, end=segment.start)) # keep track of the end of former segment end = segment.end # add final gap (if not empty) timeline.add(Segment(start=end, end=focus.end)) # other_timeline - timeline elif isinstance(focus, Timeline): # starts with an empty timeline timeline = self.empty() # add gaps for every segment in coverage of provided timeline for segment in focus.coverage(): timeline.update(self.gaps(focus=segment)) return timeline def segmentation(self): """Non-overlapping timeline Create the unique timeline with same coverage and same set of segment boundaries as original timeline, but with no overlapping segments. A picture is worth a thousand words: Original timeline: |------| |------| |----| |--| |-----| |----------| Non-overlapping timeline |-|--|-| |-|---|--| |--|----|--| Returns ------- timeline : Timeline Examples -------- >>> timeline = Timeline() >>> timeline += [Segment(0, 1), Segment(1, 2), Segment(2,3)] >>> timeline += [Segment(2, 4), Segment(6, 7)] >>> print timeline.segmentation() [ [0 --> 1] [1 --> 2] [2 --> 3] [3 --> 4] [6 --> 7] ] """ # COMPLEXITY: O(n) coverage = self.coverage() # 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: 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 coverage.overlapping(segment.middle): segments.append(segment) # next segment... start = end timeline._segments.update(segments) return timeline def for_json(self): 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 @classmethod def from_json(cls, data): 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): from pyannote.core.notebook import repr_timeline return repr_timeline(self)
class Timeline(object): """ 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 segments uri : string, optional name of segmented resource Returns ------- timeline : Timeline New timeline Examples -------- Create a new empty timeline >>> timeline = Timeline() >>> if not timeline: ... print "Timeline is empty." Timeline is empty. Add one segment (+=) >>> segment = Segment(0, 1) >>> timeline.add(segment) >>> if len(timeline) == 1: ... print "Timeline contains only one segment." Timeline contains only one segment. Add all segments from another timeline >>> other_timeline = Timeline([Segment(0.5, 3), Segment(6, 8)]) >>> timeline.update(other_timeline) Get timeline extent, coverage & duration >>> extent = timeline.extent() >>> print extent [0 --> 8] >>> coverage = timeline.coverage() >>> print coverage [ [0 --> 3] [6 --> 8] ] >>> duration = timeline.duration() >>> print "Timeline covers a total of %g seconds." % duration Timeline covers a total of 5 seconds. Iterate over (sorted) timeline segments >>> for segment in timeline: ... print segment [0 --> 1] [0.5 --> 3] [6 --> 8] Segmentation >>> segmentation = timeline.segmentation() >>> print segmentation [ [0 --> 0.5] [0.5 --> 1] [1 --> 3] [6 --> 8] ] Gaps >>> timeline = timeline.copy() >>> print timeline [ [0 --> 1] [0.5 --> 3] [6 --> 8] ] >>> print timeline.gaps() [ [3 --> 6] ] >>> segment = Segment(0, 10) >>> print timeline.gaps(segment) [ [3 --> 6] [8 --> 10] ] """ def __init__(self, segments=None, uri=None): super(Timeline, self).__init__() # sorted set of segments (as an augmented red-black tree) segments = [s for s in segments if s] if segments else [] self._segments = SortedSet(items=segments, key_type=(float, float), updator=TimelineUpdator) # path to (or any identifier of) segmented resource self.uri = uri def __len__(self): return self._segments.length() def __nonzero__(self): return self._segments.length() > 0 def __iter__(self): return iter(self._segments) def __getitem__(self, k): """Returns kth segment""" return self._segments.kth(k) def __eq__(self, other): return self._segments == other._segments def __ne__(self, other): return self._segments != other._segments def index(self, segment): """Index of segment Parameter --------- segment : Segment Raises ------ ValueError if the segment is not present """ return self._segments.index(segment) def add(self, segment): """Add segment""" if segment: self._segments.add(segment) def update(self, timeline): """Add `timeline` segments""" self._segments.update(timeline._segments) def union(self, other): """Create new timeline made of union of segments""" segments = self._segments.union(other._segments) return Timeline(segments=segments, uri=self.uri) def co_iter(self, other): for segment, other_segment in self._segments.co_iter(other._segments): yield segment, other_segment def crop(self, other, mode='intersection', mapping=False): if isinstance(other, Segment): other = Timeline(segments=[other], uri=self.uri) return self.crop(other, mode=mode, mapping=mapping) elif isinstance(other, Timeline): if mode == 'loose': segments = [segment for segment, _ in self.co_iter(other)] return Timeline(segments=segments, uri=self.uri) elif mode == 'strict': segments = [ segment for segment, other_segment in self.co_iter(other) if segment in other_segment ] return Timeline(segments=segments, uri=self.uri) elif mode == 'intersection': if mapping: mapping = {} for segment, other_segment in self.co_iter(other): inter = segment & other_segment mapping[inter] = mapping.get(inter, list()) + [segment] return Timeline(segments=mapping, uri=self.uri), mapping else: segments = [ segment & other_segment for segment, other_segment in self.co_iter(other) ] return Timeline(segments=segments, uri=self.uri) else: raise NotImplementedError("unsupported mode: '%s'" % mode) def overlapping(self, timestamp): """Get list of segments overlapping `timestamp`""" return self._segments.overlapping(timestamp) def __str__(self): """Human-friendly representation""" string = "[\n" for segment in self._segments: string += " %s\n" % str(segment) string += "]" return string def __repr__(self): return "<Timeline(uri=%s, segments=%s)>" % (self.uri, list(self._segments)) def __contains__(self, included): """Inclusion Use expression 'segment in timeline' or 'other_timeline in timeline' Parameters ---------- included : `Segment` or `Timeline` Returns ------- contains : bool True if every segment in `included` exists in timeline, False otherwise """ if isinstance(included, Segment): return included in self._segments elif isinstance(included, Timeline): return self._segments.issuperset(included._segments) else: raise TypeError() def empty(self): """Empty copy of a timeline. Examples -------- >>> timeline = Timeline(uri="MyVideo.avi") >>> timeline += [Segment(0, 1), Segment(2, 3)] >>> empty = timeline.empty() >>> print empty.uri MyVideo.avi >>> print empty [ ] """ return Timeline(uri=self.uri) def copy(self, segment_func=None): """Duplicate timeline. If segment_func is provided, apply it to each segment first. Parameters ---------- segment_func : function Returns ------- timeline : Timeline A (possibly modified) copy of the timeline Examples -------- >>> timeline = Timeline(uri="MyVideo.avi") >>> timeline += [Segment(0, 1), Segment(2, 3)] >>> cp = timeline.copy() >>> print cp.uri MyVideo.avi >>> print cp [ [0 --> 1] [2 --> 3] ] """ # if segment_func is not provided # just add every segment if segment_func is None: return Timeline(segments=self._segments, uri=self.uri) # if is provided # apply it to each segment before adding them else: return Timeline(segments=[segment_func(s) for s in self._segments], uri=self.uri) def extent(self): """Timeline 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. Returns ------- extent : Segment Timeline extent Examples -------- >>> timeline = Timeline(uri="MyVideo.avi") >>> timeline += [Segment(0, 1), Segment(9, 10)] >>> print timeline.extent() [0 --> 10] """ return self._segments.extent() def coverage(self): """Timeline coverage The coverage of 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. Returns ------- coverage : Timeline Timeline coverage """ # make sure URI attribute is kept. coverage = Timeline(uri=self.uri) # The coverage of an empty timeline is an empty timeline. if not self: return coverage # 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 coverage segment # as very first segment of the timeline new_segment = self._segments.kth(0) for segment in self: # If there is no gap between new coverage segment and next segment, if not (segment ^ new_segment): # Extend new coverage segment using next segment new_segment |= segment # If there actually is a gap, else: # Add new segment to the timeline coverage coverage.add(new_segment) # Initialize new coverage segment as next segment # (right after the gap) new_segment = segment # Add new segment to the timeline coverage coverage.add(new_segment) return coverage def duration(self): """Timeline duration Returns ------- duration : float Duration of timeline coverage, in seconds. """ # The timeline duration is the sum of the durations # of the segments in the timeline coverage. return sum([s.duration for s in self.coverage()]) def gaps(self, focus=None): """Timeline gaps Parameters ---------- focus : None, Segment or Timeline Returns ------- gaps : Timeline Timeline made of all gaps from original timeline, and delimited by provided segment or timeline. Raises ------ TypeError when `focus` is neither None, Segment nor Timeline Examples -------- """ if focus is None: focus = self.extent() if not isinstance(focus, (Segment, Timeline)): raise TypeError("unsupported operand type(s) for -':" "%s and Timeline." % type(focus).__name__) # segment focus if isinstance(focus, Segment): # starts with an empty timeline timeline = self.empty() # `end` is meant to store the end time of former segment # initialize it with beginning of provided segment `focus` end = focus.start # focus on the intersection of timeline and provided segment for segment in self.crop(focus, mode='intersection').coverage(): # add gap between each pair of consecutive segments # if there is no gap, segment is empty, therefore not added timeline.add(Segment(start=end, end=segment.start)) # keep track of the end of former segment end = segment.end # add final gap (if not empty) timeline.add(Segment(start=end, end=focus.end)) # other_timeline - timeline elif isinstance(focus, Timeline): # starts with an empty timeline timeline = self.empty() # add gaps for every segment in coverage of provided timeline for segment in focus.coverage(): timeline.update(self.gaps(focus=segment)) return timeline def segmentation(self): """Non-overlapping timeline Create the unique timeline with same coverage and same set of segment boundaries as original timeline, but with no overlapping segments. A picture is worth a thousand words: Original timeline: |------| |------| |----| |--| |-----| |----------| Non-overlapping timeline |-|--|-| |-|---|--| |--|----|--| Returns ------- timeline : Timeline Examples -------- >>> timeline = Timeline() >>> timeline += [Segment(0, 1), Segment(1, 2), Segment(2,3)] >>> timeline += [Segment(2, 4), Segment(6, 7)] >>> print timeline.segmentation() [ [0 --> 1] [1 --> 2] [2 --> 3] [3 --> 4] [6 --> 7] ] """ # COMPLEXITY: O(n) coverage = self.coverage() # 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: 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 coverage.overlapping(segment.middle): segments.append(segment) # next segment... start = end timeline._segments.update(segments) return timeline def for_json(self): data = {PYANNOTE_JSON_TIMELINE: [s.for_json() for s in self]} if self.uri: data[PYANNOTE_URI] = self.uri return data @classmethod def from_json(cls, data): segments = [Segment.from_json(s) for s in data[PYANNOTE_JSON_TIMELINE]] uri = data.get(PYANNOTE_URI, None) return cls(segments=segments, uri=uri) def _repr_png_(self): from pyannote.core.notebook import repr_timeline return repr_timeline(self)