We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Loading CutSet from manifest may drop supervisions
Calling `CutSet.from_manifests()` on pre-computed manifests may result in dopping supervisions. 
Steps to reproduce:

```
ROOT = "."
MANIFESTS = $(ROOT)/LJSpeech-1.1/lhotse_manifests
FEATS = $(ROOT)/LJSpeech-1.1/feats

lhotse obtain ljspeech $(ROOT)
lhotse prepare ljspeech $(ROOT)/LJSpeech-1.1/ $(MANIFESTS)

# Extract LibrosaFbank features
lhotse feat write-default-config $(MANIFESTS)/librosa_config.yaml -f librosa-fbank
lhotse feat extract \
$(MANIFESTS)/recordings.json \
$(FEATS) \
-f $(MANIFESTS)/librosa_config.yaml

# store cuts
lhotse cut simple $(MANIFESTS)/cuts.json \
-r $(MANIFESTS)/recordings.json \
-f $(FEATS)/feature_manifest.json.gz \
-s $(MANIFESTS)/supervisions.json
```
The stored cuts don't have supervisions. The problem is that the ends of the feature segments and ends of the supervision segments do not match, because Supervision.end [rounds to 8 digits](https://github.com/lhotse-speech/lhotse/blob/61177e48cbc2e53f83541ed8b301d4d4ab1b54da/lhotse/supervision.py#L27), but the Features.end does not.

The SupervisionSegment.find then does not match features with supervisions correctly in [SupervisionSet.find()](https://github.com/lhotse-speech/lhotse/blob/61177e48cbc2e53f83541ed8b301d4d4ab1b54da/lhotse/cut.py#L1286). 

I tried to disable the rounding, but it triggers the `test/cut/test_cut_truncate.py` test. Quick fix would be to round digits inside the find function or use some tolerance, but I am not sure if this would not break something else. WYT @pzelasko ?


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `lhotse/supervision.py`
Content:
```
1 import logging
2 from dataclasses import dataclass
3 from itertools import islice
4 from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence
5 
6 from lhotse.serialization import Serializable
7 from lhotse.utils import Seconds, asdict_nonull, exactly_one_not_null, fastcopy, \
8     index_by_id_and_check, \
9     perturb_num_samples, split_sequence
10 
11 
12 @dataclass(frozen=True, unsafe_hash=True)
13 class SupervisionSegment:
14     id: str
15     recording_id: str
16     start: Seconds
17     duration: Seconds
18     channel: int = 0
19     text: Optional[str] = None
20     language: Optional[str] = None
21     speaker: Optional[str] = None
22     gender: Optional[str] = None
23     custom: Optional[Dict[str, Any]] = None
24 
25     @property
26     def end(self) -> Seconds:
27         return round(self.start + self.duration, ndigits=8)
28 
29     def with_offset(self, offset: Seconds) -> 'SupervisionSegment':
30         """Return an identical ``SupervisionSegment``, but with the ``offset`` added to the ``start`` field."""
31         return fastcopy(self, start=round(self.start + offset, ndigits=8))
32 
33     def perturb_speed(
34             self,
35             factor: float,
36             sampling_rate: int,
37             affix_id: bool = True
38     ) -> 'SupervisionSegment':
39         """
40         Return a ``SupervisionSegment`` that has time boundaries matching the
41         recording/cut perturbed with the same factor.
42 
43         :param factor: The speed will be adjusted this many times (e.g. factor=1.1 means 1.1x faster).
44         :param sampling_rate: The sampling rate is necessary to accurately perturb the start
45             and duration (going through the sample counts).
46         :param affix_id: When true, we will modify the ``id`` and ``recording_id`` fields
47             by affixing it with "_sp{factor}".
48         :return: a modified copy of the current ``Recording``.
49         """
50         start_sample = round(self.start * sampling_rate)
51         num_samples = round(self.duration * sampling_rate)
52         new_start = perturb_num_samples(start_sample, factor) / sampling_rate
53         new_duration = perturb_num_samples(num_samples, factor) / sampling_rate
54         return fastcopy(
55             self,
56             id=f'{self.id}_sp{factor}' if affix_id else self.id,
57             recording_id=f'{self.recording_id}_sp{factor}' if affix_id else self.id,
58             start=new_start,
59             duration=new_duration
60         )
61 
62     def trim(self, end: Seconds) -> 'SupervisionSegment':
63         """
64         Return an identical ``SupervisionSegment``, but ensure that ``self.start`` is not negative (in which case
65         it's set to 0) and ``self.end`` does not exceed the ``end`` parameter.
66 
67         This method is useful for ensuring that the supervision does not exceed a cut's bounds,
68         in which case pass ``cut.duration`` as the ``end`` argument, since supervision times are relative to the cut.
69         """
70         start_exceeds_by = abs(min(0, self.start))
71         end_exceeds_by = max(0, self.end - end)
72         return fastcopy(self, start=max(0, self.start), duration=self.duration - end_exceeds_by - start_exceeds_by)
73 
74     def map(self, transform_fn: Callable[['SupervisionSegment'], 'SupervisionSegment']) -> 'SupervisionSegment':
75         """
76         Return a copy of the current segment, transformed with ``transform_fn``.
77 
78         :param transform_fn: a function that takes a segment as input, transforms it and returns a new segment.
79         :return: a modified ``SupervisionSegment``.
80         """
81         return transform_fn(self)
82 
83     def transform_text(self, transform_fn: Callable[[str], str]) -> 'SupervisionSegment':
84         """
85         Return a copy of the current segment with transformed ``text`` field.
86         Useful for text normalization, phonetic transcription, etc.
87 
88         :param transform_fn: a function that accepts a string and returns a string.
89         :return: a ``SupervisionSegment`` with adjusted text.
90         """
91         if self.text is None:
92             return self
93         return fastcopy(self, text=transform_fn(self.text))
94 
95     @staticmethod
96     def from_dict(data: dict) -> 'SupervisionSegment':
97         return SupervisionSegment(**data)
98 
99 
100 @dataclass
101 class SupervisionSet(Serializable, Sequence[SupervisionSegment]):
102     """
103     SupervisionSet represents a collection of segments containing some supervision information.
104     The only required fields are the ID of the segment, ID of the corresponding recording,
105     and the start and duration of the segment in seconds.
106     All other fields, such as text, language or speaker, are deliberately optional
107     to support a wide range of tasks, as well as adding more supervision types in the future,
108     while retaining backwards compatibility.
109     """
110     segments: Dict[str, SupervisionSegment]
111 
112     @staticmethod
113     def from_segments(segments: Iterable[SupervisionSegment]) -> 'SupervisionSet':
114         return SupervisionSet(segments=index_by_id_and_check(segments))
115 
116     @staticmethod
117     def from_dicts(data: Iterable[Dict]) -> 'SupervisionSet':
118         return SupervisionSet.from_segments(SupervisionSegment.from_dict(s) for s in data)
119 
120     def to_dicts(self) -> List[dict]:
121         return [asdict_nonull(s) for s in self]
122 
123     def split(self, num_splits: int, shuffle: bool = False) -> List['SupervisionSet']:
124         """
125         Split the ``SupervisionSet`` into ``num_splits`` pieces of equal size.
126 
127         :param num_splits: Requested number of splits.
128         :param shuffle: Optionally shuffle the supervisions order first.
129         :return: A list of ``SupervisionSet`` pieces.
130         """
131         return [
132             SupervisionSet.from_segments(subset) for subset in
133             split_sequence(self, num_splits=num_splits, shuffle=shuffle)
134         ]
135 
136     def subset(self, first: Optional[int] = None, last: Optional[int] = None) -> 'SupervisionSet':
137         """
138         Return a new ``SupervisionSet`` according to the selected subset criterion.
139         Only a single argument to ``subset`` is supported at this time.
140 
141         :param first: int, the number of first supervisions to keep.
142         :param last: int, the number of last supervisions to keep.
143         :return: a new ``SupervisionSet`` with the subset results.
144         """
145         assert exactly_one_not_null(first, last), "subset() can handle only one non-None arg."
146 
147         if first is not None:
148             assert first > 0
149             if first > len(self):
150                 logging.warning(f'SupervisionSet has only {len(self)} items but first {first} required; '
151                                 f'not doing anything.')
152                 return self
153             return SupervisionSet.from_segments(islice(self, first))
154 
155         if last is not None:
156             assert last > 0
157             if last > len(self):
158                 logging.warning(f'SupervisionSet has only {len(self)} items but last {last} required; '
159                                 f'not doing anything.')
160                 return self
161             return SupervisionSet.from_segments(islice(self, len(self) - last, len(self)))
162 
163     def filter(self, predicate: Callable[[SupervisionSegment], bool]) -> 'SupervisionSet':
164         """
165         Return a new SupervisionSet with the SupervisionSegments that satisfy the `predicate`.
166 
167         :param predicate: a function that takes a supervision as an argument and returns bool.
168         :return: a filtered SupervisionSet.
169         """
170         return SupervisionSet.from_segments(seg for seg in self if predicate(seg))
171 
172     def map(self, transform_fn: Callable[[SupervisionSegment], SupervisionSegment]) -> 'SupervisionSet':
173         """
174         Map a ``transform_fn`` to the SupervisionSegments and return a new ``SupervisionSet``.
175 
176         :param transform_fn: a function that modifies a supervision as an argument.
177         :return: a new ``SupervisionSet`` with modified segments.
178         """
179         return SupervisionSet.from_segments(s.map(transform_fn) for s in self)
180 
181     def transform_text(self, transform_fn: Callable[[str], str]) -> 'SupervisionSet':
182         """
183         Return a copy of the current ``SupervisionSet`` with the segments having a transformed ``text`` field.
184         Useful for text normalization, phonetic transcription, etc.
185 
186         :param transform_fn: a function that accepts a string and returns a string.
187         :return: a ``SupervisionSet`` with adjusted text.
188         """
189         return SupervisionSet.from_segments(s.transform_text(transform_fn) for s in self)
190 
191     def find(
192             self,
193             recording_id: str,
194             channel: Optional[int] = None,
195             start_after: Seconds = 0,
196             end_before: Optional[Seconds] = None,
197             adjust_offset: bool = False
198     ) -> Iterable[SupervisionSegment]:
199         """
200         Return an iterable of segments that match the provided ``recording_id``.
201 
202         :param recording_id: Desired recording ID.
203         :param channel: When specified, return supervisions in that channel - otherwise, in all channels.
204         :param start_after: When specified, return segments that start after the given value.
205         :param end_before: When specified, return segments that end before the given value.
206         :param adjust_offset: When true, return segments as if the recordings had started at ``start_after``.
207             This is useful for creating Cuts. Fom a user perspective, when dealing with a Cut, it is no
208             longer helpful to know when the supervisions starts in a recording - instead, it's useful to
209             know when the supervision starts relative to the start of the Cut.
210             In the anticipated use-case, ``start_after`` and ``end_before`` would be
211             the beginning and end of a cut;
212             this option converts the times to be relative to the start of the cut.
213         :return: An iterator over supervision segments satisfying all criteria.
214         """
215         segment_by_recording_id = self._index_by_recording_id_and_cache()
216         return (
217             # We only modify the offset - the duration remains the same, as we're only shifting the segment
218             # relative to the Cut's start, and not truncating anything.
219             segment.with_offset(-start_after) if adjust_offset else segment
220             for segment in segment_by_recording_id.get(recording_id, [])
221             if (channel is None or segment.channel == channel)
222                and segment.start >= start_after
223                and (end_before is None or segment.end <= end_before)
224         )
225 
226     # This is a cache that significantly speeds up repeated ``find()`` queries.
227     _segments_by_recording_id: Optional[Dict[str, List[SupervisionSegment]]] = None
228 
229     def _index_by_recording_id_and_cache(self):
230         if self._segments_by_recording_id is None:
231             from cytoolz import groupby
232             self._segments_by_recording_id = groupby(lambda seg: seg.recording_id, self)
233         return self._segments_by_recording_id
234 
235     def __repr__(self) -> str:
236         return f'SupervisionSet(len={len(self)})'
237 
238     def __getitem__(self, item: str) -> SupervisionSegment:
239         return self.segments[item]
240 
241     def __iter__(self) -> Iterable[SupervisionSegment]:
242         return iter(self.segments.values())
243 
244     def __len__(self) -> int:
245         return len(self.segments)
246 
247     def __add__(self, other: 'SupervisionSet') -> 'SupervisionSet':
248         return SupervisionSet(segments={**self.segments, **other.segments})
249
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/lhotse/supervision.py b/lhotse/supervision.py
--- a/lhotse/supervision.py
+++ b/lhotse/supervision.py
@@ -194,7 +194,8 @@
             channel: Optional[int] = None,
             start_after: Seconds = 0,
             end_before: Optional[Seconds] = None,
-            adjust_offset: bool = False
+            adjust_offset: bool = False,
+            tolerance: Seconds = 0.001
     ) -> Iterable[SupervisionSegment]:
         """
         Return an iterable of segments that match the provided ``recording_id``.
@@ -210,6 +211,8 @@
             In the anticipated use-case, ``start_after`` and ``end_before`` would be
             the beginning and end of a cut;
             this option converts the times to be relative to the start of the cut.
+        :param tolerance: Additional margin to account for floating point rounding errors
+            when comparing segment boundaries.
         :return: An iterator over supervision segments satisfying all criteria.
         """
         segment_by_recording_id = self._index_by_recording_id_and_cache()
@@ -219,8 +222,8 @@
             segment.with_offset(-start_after) if adjust_offset else segment
             for segment in segment_by_recording_id.get(recording_id, [])
             if (channel is None or segment.channel == channel)
-               and segment.start >= start_after
-               and (end_before is None or segment.end <= end_before)
+               and segment.start >= start_after - tolerance
+               and (end_before is None or segment.end <= end_before + tolerance)
         )
 
     # This is a cache that significantly speeds up repeated ``find()`` queries.

{"golden_diff": "diff --git a/lhotse/supervision.py b/lhotse/supervision.py\n--- a/lhotse/supervision.py\n+++ b/lhotse/supervision.py\n@@ -194,7 +194,8 @@\n             channel: Optional[int] = None,\n             start_after: Seconds = 0,\n             end_before: Optional[Seconds] = None,\n-            adjust_offset: bool = False\n+            adjust_offset: bool = False,\n+            tolerance: Seconds = 0.001\n     ) -> Iterable[SupervisionSegment]:\n         \"\"\"\n         Return an iterable of segments that match the provided ``recording_id``.\n@@ -210,6 +211,8 @@\n             In the anticipated use-case, ``start_after`` and ``end_before`` would be\n             the beginning and end of a cut;\n             this option converts the times to be relative to the start of the cut.\n+        :param tolerance: Additional margin to account for floating point rounding errors\n+            when comparing segment boundaries.\n         :return: An iterator over supervision segments satisfying all criteria.\n         \"\"\"\n         segment_by_recording_id = self._index_by_recording_id_and_cache()\n@@ -219,8 +222,8 @@\n             segment.with_offset(-start_after) if adjust_offset else segment\n             for segment in segment_by_recording_id.get(recording_id, [])\n             if (channel is None or segment.channel == channel)\n-               and segment.start >= start_after\n-               and (end_before is None or segment.end <= end_before)\n+               and segment.start >= start_after - tolerance\n+               and (end_before is None or segment.end <= end_before + tolerance)\n         )\n \n     # This is a cache that significantly speeds up repeated ``find()`` queries.\n", "issue": "Loading CutSet from manifest may drop supervisions\nCalling `CutSet.from_manifests()` on pre-computed manifests may result in dopping supervisions. \r\nSteps to reproduce:\r\n\r\n```\r\nROOT = \".\"\r\nMANIFESTS = $(ROOT)/LJSpeech-1.1/lhotse_manifests\r\nFEATS = $(ROOT)/LJSpeech-1.1/feats\r\n\r\nlhotse obtain ljspeech $(ROOT)\r\nlhotse prepare ljspeech $(ROOT)/LJSpeech-1.1/ $(MANIFESTS)\r\n\r\n# Extract LibrosaFbank features\r\nlhotse feat write-default-config $(MANIFESTS)/librosa_config.yaml -f librosa-fbank\r\nlhotse feat extract \\\r\n$(MANIFESTS)/recordings.json \\\r\n$(FEATS) \\\r\n-f $(MANIFESTS)/librosa_config.yaml\r\n\r\n# store cuts\r\nlhotse cut simple $(MANIFESTS)/cuts.json \\\r\n-r $(MANIFESTS)/recordings.json \\\r\n-f $(FEATS)/feature_manifest.json.gz \\\r\n-s $(MANIFESTS)/supervisions.json\r\n```\r\nThe stored cuts don't have supervisions. The problem is that the ends of the feature segments and ends of the supervision segments do not match, because Supervision.end [rounds to 8 digits](https://github.com/lhotse-speech/lhotse/blob/61177e48cbc2e53f83541ed8b301d4d4ab1b54da/lhotse/supervision.py#L27), but the Features.end does not.\r\n\r\nThe SupervisionSegment.find then does not match features with supervisions correctly in [SupervisionSet.find()](https://github.com/lhotse-speech/lhotse/blob/61177e48cbc2e53f83541ed8b301d4d4ab1b54da/lhotse/cut.py#L1286). \r\n\r\nI tried to disable the rounding, but it triggers the `test/cut/test_cut_truncate.py` test. Quick fix would be to round digits inside the find function or use some tolerance, but I am not sure if this would not break something else. WYT @pzelasko ?\r\n\n", "before_files": [{"content": "import logging\nfrom dataclasses import dataclass\nfrom itertools import islice\nfrom typing import Any, Callable, Dict, Iterable, List, Optional, Sequence\n\nfrom lhotse.serialization import Serializable\nfrom lhotse.utils import Seconds, asdict_nonull, exactly_one_not_null, fastcopy, \\\n    index_by_id_and_check, \\\n    perturb_num_samples, split_sequence\n\n\n@dataclass(frozen=True, unsafe_hash=True)\nclass SupervisionSegment:\n    id: str\n    recording_id: str\n    start: Seconds\n    duration: Seconds\n    channel: int = 0\n    text: Optional[str] = None\n    language: Optional[str] = None\n    speaker: Optional[str] = None\n    gender: Optional[str] = None\n    custom: Optional[Dict[str, Any]] = None\n\n    @property\n    def end(self) -> Seconds:\n        return round(self.start + self.duration, ndigits=8)\n\n    def with_offset(self, offset: Seconds) -> 'SupervisionSegment':\n        \"\"\"Return an identical ``SupervisionSegment``, but with the ``offset`` added to the ``start`` field.\"\"\"\n        return fastcopy(self, start=round(self.start + offset, ndigits=8))\n\n    def perturb_speed(\n            self,\n            factor: float,\n            sampling_rate: int,\n            affix_id: bool = True\n    ) -> 'SupervisionSegment':\n        \"\"\"\n        Return a ``SupervisionSegment`` that has time boundaries matching the\n        recording/cut perturbed with the same factor.\n\n        :param factor: The speed will be adjusted this many times (e.g. factor=1.1 means 1.1x faster).\n        :param sampling_rate: The sampling rate is necessary to accurately perturb the start\n            and duration (going through the sample counts).\n        :param affix_id: When true, we will modify the ``id`` and ``recording_id`` fields\n            by affixing it with \"_sp{factor}\".\n        :return: a modified copy of the current ``Recording``.\n        \"\"\"\n        start_sample = round(self.start * sampling_rate)\n        num_samples = round(self.duration * sampling_rate)\n        new_start = perturb_num_samples(start_sample, factor) / sampling_rate\n        new_duration = perturb_num_samples(num_samples, factor) / sampling_rate\n        return fastcopy(\n            self,\n            id=f'{self.id}_sp{factor}' if affix_id else self.id,\n            recording_id=f'{self.recording_id}_sp{factor}' if affix_id else self.id,\n            start=new_start,\n            duration=new_duration\n        )\n\n    def trim(self, end: Seconds) -> 'SupervisionSegment':\n        \"\"\"\n        Return an identical ``SupervisionSegment``, but ensure that ``self.start`` is not negative (in which case\n        it's set to 0) and ``self.end`` does not exceed the ``end`` parameter.\n\n        This method is useful for ensuring that the supervision does not exceed a cut's bounds,\n        in which case pass ``cut.duration`` as the ``end`` argument, since supervision times are relative to the cut.\n        \"\"\"\n        start_exceeds_by = abs(min(0, self.start))\n        end_exceeds_by = max(0, self.end - end)\n        return fastcopy(self, start=max(0, self.start), duration=self.duration - end_exceeds_by - start_exceeds_by)\n\n    def map(self, transform_fn: Callable[['SupervisionSegment'], 'SupervisionSegment']) -> 'SupervisionSegment':\n        \"\"\"\n        Return a copy of the current segment, transformed with ``transform_fn``.\n\n        :param transform_fn: a function that takes a segment as input, transforms it and returns a new segment.\n        :return: a modified ``SupervisionSegment``.\n        \"\"\"\n        return transform_fn(self)\n\n    def transform_text(self, transform_fn: Callable[[str], str]) -> 'SupervisionSegment':\n        \"\"\"\n        Return a copy of the current segment with transformed ``text`` field.\n        Useful for text normalization, phonetic transcription, etc.\n\n        :param transform_fn: a function that accepts a string and returns a string.\n        :return: a ``SupervisionSegment`` with adjusted text.\n        \"\"\"\n        if self.text is None:\n            return self\n        return fastcopy(self, text=transform_fn(self.text))\n\n    @staticmethod\n    def from_dict(data: dict) -> 'SupervisionSegment':\n        return SupervisionSegment(**data)\n\n\n@dataclass\nclass SupervisionSet(Serializable, Sequence[SupervisionSegment]):\n    \"\"\"\n    SupervisionSet represents a collection of segments containing some supervision information.\n    The only required fields are the ID of the segment, ID of the corresponding recording,\n    and the start and duration of the segment in seconds.\n    All other fields, such as text, language or speaker, are deliberately optional\n    to support a wide range of tasks, as well as adding more supervision types in the future,\n    while retaining backwards compatibility.\n    \"\"\"\n    segments: Dict[str, SupervisionSegment]\n\n    @staticmethod\n    def from_segments(segments: Iterable[SupervisionSegment]) -> 'SupervisionSet':\n        return SupervisionSet(segments=index_by_id_and_check(segments))\n\n    @staticmethod\n    def from_dicts(data: Iterable[Dict]) -> 'SupervisionSet':\n        return SupervisionSet.from_segments(SupervisionSegment.from_dict(s) for s in data)\n\n    def to_dicts(self) -> List[dict]:\n        return [asdict_nonull(s) for s in self]\n\n    def split(self, num_splits: int, shuffle: bool = False) -> List['SupervisionSet']:\n        \"\"\"\n        Split the ``SupervisionSet`` into ``num_splits`` pieces of equal size.\n\n        :param num_splits: Requested number of splits.\n        :param shuffle: Optionally shuffle the supervisions order first.\n        :return: A list of ``SupervisionSet`` pieces.\n        \"\"\"\n        return [\n            SupervisionSet.from_segments(subset) for subset in\n            split_sequence(self, num_splits=num_splits, shuffle=shuffle)\n        ]\n\n    def subset(self, first: Optional[int] = None, last: Optional[int] = None) -> 'SupervisionSet':\n        \"\"\"\n        Return a new ``SupervisionSet`` according to the selected subset criterion.\n        Only a single argument to ``subset`` is supported at this time.\n\n        :param first: int, the number of first supervisions to keep.\n        :param last: int, the number of last supervisions to keep.\n        :return: a new ``SupervisionSet`` with the subset results.\n        \"\"\"\n        assert exactly_one_not_null(first, last), \"subset() can handle only one non-None arg.\"\n\n        if first is not None:\n            assert first > 0\n            if first > len(self):\n                logging.warning(f'SupervisionSet has only {len(self)} items but first {first} required; '\n                                f'not doing anything.')\n                return self\n            return SupervisionSet.from_segments(islice(self, first))\n\n        if last is not None:\n            assert last > 0\n            if last > len(self):\n                logging.warning(f'SupervisionSet has only {len(self)} items but last {last} required; '\n                                f'not doing anything.')\n                return self\n            return SupervisionSet.from_segments(islice(self, len(self) - last, len(self)))\n\n    def filter(self, predicate: Callable[[SupervisionSegment], bool]) -> 'SupervisionSet':\n        \"\"\"\n        Return a new SupervisionSet with the SupervisionSegments that satisfy the `predicate`.\n\n        :param predicate: a function that takes a supervision as an argument and returns bool.\n        :return: a filtered SupervisionSet.\n        \"\"\"\n        return SupervisionSet.from_segments(seg for seg in self if predicate(seg))\n\n    def map(self, transform_fn: Callable[[SupervisionSegment], SupervisionSegment]) -> 'SupervisionSet':\n        \"\"\"\n        Map a ``transform_fn`` to the SupervisionSegments and return a new ``SupervisionSet``.\n\n        :param transform_fn: a function that modifies a supervision as an argument.\n        :return: a new ``SupervisionSet`` with modified segments.\n        \"\"\"\n        return SupervisionSet.from_segments(s.map(transform_fn) for s in self)\n\n    def transform_text(self, transform_fn: Callable[[str], str]) -> 'SupervisionSet':\n        \"\"\"\n        Return a copy of the current ``SupervisionSet`` with the segments having a transformed ``text`` field.\n        Useful for text normalization, phonetic transcription, etc.\n\n        :param transform_fn: a function that accepts a string and returns a string.\n        :return: a ``SupervisionSet`` with adjusted text.\n        \"\"\"\n        return SupervisionSet.from_segments(s.transform_text(transform_fn) for s in self)\n\n    def find(\n            self,\n            recording_id: str,\n            channel: Optional[int] = None,\n            start_after: Seconds = 0,\n            end_before: Optional[Seconds] = None,\n            adjust_offset: bool = False\n    ) -> Iterable[SupervisionSegment]:\n        \"\"\"\n        Return an iterable of segments that match the provided ``recording_id``.\n\n        :param recording_id: Desired recording ID.\n        :param channel: When specified, return supervisions in that channel - otherwise, in all channels.\n        :param start_after: When specified, return segments that start after the given value.\n        :param end_before: When specified, return segments that end before the given value.\n        :param adjust_offset: When true, return segments as if the recordings had started at ``start_after``.\n            This is useful for creating Cuts. Fom a user perspective, when dealing with a Cut, it is no\n            longer helpful to know when the supervisions starts in a recording - instead, it's useful to\n            know when the supervision starts relative to the start of the Cut.\n            In the anticipated use-case, ``start_after`` and ``end_before`` would be\n            the beginning and end of a cut;\n            this option converts the times to be relative to the start of the cut.\n        :return: An iterator over supervision segments satisfying all criteria.\n        \"\"\"\n        segment_by_recording_id = self._index_by_recording_id_and_cache()\n        return (\n            # We only modify the offset - the duration remains the same, as we're only shifting the segment\n            # relative to the Cut's start, and not truncating anything.\n            segment.with_offset(-start_after) if adjust_offset else segment\n            for segment in segment_by_recording_id.get(recording_id, [])\n            if (channel is None or segment.channel == channel)\n               and segment.start >= start_after\n               and (end_before is None or segment.end <= end_before)\n        )\n\n    # This is a cache that significantly speeds up repeated ``find()`` queries.\n    _segments_by_recording_id: Optional[Dict[str, List[SupervisionSegment]]] = None\n\n    def _index_by_recording_id_and_cache(self):\n        if self._segments_by_recording_id is None:\n            from cytoolz import groupby\n            self._segments_by_recording_id = groupby(lambda seg: seg.recording_id, self)\n        return self._segments_by_recording_id\n\n    def __repr__(self) -> str:\n        return f'SupervisionSet(len={len(self)})'\n\n    def __getitem__(self, item: str) -> SupervisionSegment:\n        return self.segments[item]\n\n    def __iter__(self) -> Iterable[SupervisionSegment]:\n        return iter(self.segments.values())\n\n    def __len__(self) -> int:\n        return len(self.segments)\n\n    def __add__(self, other: 'SupervisionSet') -> 'SupervisionSet':\n        return SupervisionSet(segments={**self.segments, **other.segments})\n", "path": "lhotse/supervision.py"}], "after_files": [{"content": "import logging\nfrom dataclasses import dataclass\nfrom itertools import islice\nfrom typing import Any, Callable, Dict, Iterable, List, Optional, Sequence\n\nfrom lhotse.serialization import Serializable\nfrom lhotse.utils import Seconds, asdict_nonull, exactly_one_not_null, fastcopy, \\\n    index_by_id_and_check, \\\n    perturb_num_samples, split_sequence\n\n\n@dataclass(frozen=True, unsafe_hash=True)\nclass SupervisionSegment:\n    id: str\n    recording_id: str\n    start: Seconds\n    duration: Seconds\n    channel: int = 0\n    text: Optional[str] = None\n    language: Optional[str] = None\n    speaker: Optional[str] = None\n    gender: Optional[str] = None\n    custom: Optional[Dict[str, Any]] = None\n\n    @property\n    def end(self) -> Seconds:\n        return round(self.start + self.duration, ndigits=8)\n\n    def with_offset(self, offset: Seconds) -> 'SupervisionSegment':\n        \"\"\"Return an identical ``SupervisionSegment``, but with the ``offset`` added to the ``start`` field.\"\"\"\n        return fastcopy(self, start=round(self.start + offset, ndigits=8))\n\n    def perturb_speed(\n            self,\n            factor: float,\n            sampling_rate: int,\n            affix_id: bool = True\n    ) -> 'SupervisionSegment':\n        \"\"\"\n        Return a ``SupervisionSegment`` that has time boundaries matching the\n        recording/cut perturbed with the same factor.\n\n        :param factor: The speed will be adjusted this many times (e.g. factor=1.1 means 1.1x faster).\n        :param sampling_rate: The sampling rate is necessary to accurately perturb the start\n            and duration (going through the sample counts).\n        :param affix_id: When true, we will modify the ``id`` and ``recording_id`` fields\n            by affixing it with \"_sp{factor}\".\n        :return: a modified copy of the current ``Recording``.\n        \"\"\"\n        start_sample = round(self.start * sampling_rate)\n        num_samples = round(self.duration * sampling_rate)\n        new_start = perturb_num_samples(start_sample, factor) / sampling_rate\n        new_duration = perturb_num_samples(num_samples, factor) / sampling_rate\n        return fastcopy(\n            self,\n            id=f'{self.id}_sp{factor}' if affix_id else self.id,\n            recording_id=f'{self.recording_id}_sp{factor}' if affix_id else self.id,\n            start=new_start,\n            duration=new_duration\n        )\n\n    def trim(self, end: Seconds) -> 'SupervisionSegment':\n        \"\"\"\n        Return an identical ``SupervisionSegment``, but ensure that ``self.start`` is not negative (in which case\n        it's set to 0) and ``self.end`` does not exceed the ``end`` parameter.\n\n        This method is useful for ensuring that the supervision does not exceed a cut's bounds,\n        in which case pass ``cut.duration`` as the ``end`` argument, since supervision times are relative to the cut.\n        \"\"\"\n        start_exceeds_by = abs(min(0, self.start))\n        end_exceeds_by = max(0, self.end - end)\n        return fastcopy(self, start=max(0, self.start), duration=self.duration - end_exceeds_by - start_exceeds_by)\n\n    def map(self, transform_fn: Callable[['SupervisionSegment'], 'SupervisionSegment']) -> 'SupervisionSegment':\n        \"\"\"\n        Return a copy of the current segment, transformed with ``transform_fn``.\n\n        :param transform_fn: a function that takes a segment as input, transforms it and returns a new segment.\n        :return: a modified ``SupervisionSegment``.\n        \"\"\"\n        return transform_fn(self)\n\n    def transform_text(self, transform_fn: Callable[[str], str]) -> 'SupervisionSegment':\n        \"\"\"\n        Return a copy of the current segment with transformed ``text`` field.\n        Useful for text normalization, phonetic transcription, etc.\n\n        :param transform_fn: a function that accepts a string and returns a string.\n        :return: a ``SupervisionSegment`` with adjusted text.\n        \"\"\"\n        if self.text is None:\n            return self\n        return fastcopy(self, text=transform_fn(self.text))\n\n    @staticmethod\n    def from_dict(data: dict) -> 'SupervisionSegment':\n        return SupervisionSegment(**data)\n\n\n@dataclass\nclass SupervisionSet(Serializable, Sequence[SupervisionSegment]):\n    \"\"\"\n    SupervisionSet represents a collection of segments containing some supervision information.\n    The only required fields are the ID of the segment, ID of the corresponding recording,\n    and the start and duration of the segment in seconds.\n    All other fields, such as text, language or speaker, are deliberately optional\n    to support a wide range of tasks, as well as adding more supervision types in the future,\n    while retaining backwards compatibility.\n    \"\"\"\n    segments: Dict[str, SupervisionSegment]\n\n    @staticmethod\n    def from_segments(segments: Iterable[SupervisionSegment]) -> 'SupervisionSet':\n        return SupervisionSet(segments=index_by_id_and_check(segments))\n\n    @staticmethod\n    def from_dicts(data: Iterable[Dict]) -> 'SupervisionSet':\n        return SupervisionSet.from_segments(SupervisionSegment.from_dict(s) for s in data)\n\n    def to_dicts(self) -> List[dict]:\n        return [asdict_nonull(s) for s in self]\n\n    def split(self, num_splits: int, shuffle: bool = False) -> List['SupervisionSet']:\n        \"\"\"\n        Split the ``SupervisionSet`` into ``num_splits`` pieces of equal size.\n\n        :param num_splits: Requested number of splits.\n        :param shuffle: Optionally shuffle the supervisions order first.\n        :return: A list of ``SupervisionSet`` pieces.\n        \"\"\"\n        return [\n            SupervisionSet.from_segments(subset) for subset in\n            split_sequence(self, num_splits=num_splits, shuffle=shuffle)\n        ]\n\n    def subset(self, first: Optional[int] = None, last: Optional[int] = None) -> 'SupervisionSet':\n        \"\"\"\n        Return a new ``SupervisionSet`` according to the selected subset criterion.\n        Only a single argument to ``subset`` is supported at this time.\n\n        :param first: int, the number of first supervisions to keep.\n        :param last: int, the number of last supervisions to keep.\n        :return: a new ``SupervisionSet`` with the subset results.\n        \"\"\"\n        assert exactly_one_not_null(first, last), \"subset() can handle only one non-None arg.\"\n\n        if first is not None:\n            assert first > 0\n            if first > len(self):\n                logging.warning(f'SupervisionSet has only {len(self)} items but first {first} required; '\n                                f'not doing anything.')\n                return self\n            return SupervisionSet.from_segments(islice(self, first))\n\n        if last is not None:\n            assert last > 0\n            if last > len(self):\n                logging.warning(f'SupervisionSet has only {len(self)} items but last {last} required; '\n                                f'not doing anything.')\n                return self\n            return SupervisionSet.from_segments(islice(self, len(self) - last, len(self)))\n\n    def filter(self, predicate: Callable[[SupervisionSegment], bool]) -> 'SupervisionSet':\n        \"\"\"\n        Return a new SupervisionSet with the SupervisionSegments that satisfy the `predicate`.\n\n        :param predicate: a function that takes a supervision as an argument and returns bool.\n        :return: a filtered SupervisionSet.\n        \"\"\"\n        return SupervisionSet.from_segments(seg for seg in self if predicate(seg))\n\n    def map(self, transform_fn: Callable[[SupervisionSegment], SupervisionSegment]) -> 'SupervisionSet':\n        \"\"\"\n        Map a ``transform_fn`` to the SupervisionSegments and return a new ``SupervisionSet``.\n\n        :param transform_fn: a function that modifies a supervision as an argument.\n        :return: a new ``SupervisionSet`` with modified segments.\n        \"\"\"\n        return SupervisionSet.from_segments(s.map(transform_fn) for s in self)\n\n    def transform_text(self, transform_fn: Callable[[str], str]) -> 'SupervisionSet':\n        \"\"\"\n        Return a copy of the current ``SupervisionSet`` with the segments having a transformed ``text`` field.\n        Useful for text normalization, phonetic transcription, etc.\n\n        :param transform_fn: a function that accepts a string and returns a string.\n        :return: a ``SupervisionSet`` with adjusted text.\n        \"\"\"\n        return SupervisionSet.from_segments(s.transform_text(transform_fn) for s in self)\n\n    def find(\n            self,\n            recording_id: str,\n            channel: Optional[int] = None,\n            start_after: Seconds = 0,\n            end_before: Optional[Seconds] = None,\n            adjust_offset: bool = False,\n            tolerance: Seconds = 0.001\n    ) -> Iterable[SupervisionSegment]:\n        \"\"\"\n        Return an iterable of segments that match the provided ``recording_id``.\n\n        :param recording_id: Desired recording ID.\n        :param channel: When specified, return supervisions in that channel - otherwise, in all channels.\n        :param start_after: When specified, return segments that start after the given value.\n        :param end_before: When specified, return segments that end before the given value.\n        :param adjust_offset: When true, return segments as if the recordings had started at ``start_after``.\n            This is useful for creating Cuts. Fom a user perspective, when dealing with a Cut, it is no\n            longer helpful to know when the supervisions starts in a recording - instead, it's useful to\n            know when the supervision starts relative to the start of the Cut.\n            In the anticipated use-case, ``start_after`` and ``end_before`` would be\n            the beginning and end of a cut;\n            this option converts the times to be relative to the start of the cut.\n        :param tolerance: Additional margin to account for floating point rounding errors\n            when comparing segment boundaries.\n        :return: An iterator over supervision segments satisfying all criteria.\n        \"\"\"\n        segment_by_recording_id = self._index_by_recording_id_and_cache()\n        return (\n            # We only modify the offset - the duration remains the same, as we're only shifting the segment\n            # relative to the Cut's start, and not truncating anything.\n            segment.with_offset(-start_after) if adjust_offset else segment\n            for segment in segment_by_recording_id.get(recording_id, [])\n            if (channel is None or segment.channel == channel)\n               and segment.start >= start_after - tolerance\n               and (end_before is None or segment.end <= end_before + tolerance)\n        )\n\n    # This is a cache that significantly speeds up repeated ``find()`` queries.\n    _segments_by_recording_id: Optional[Dict[str, List[SupervisionSegment]]] = None\n\n    def _index_by_recording_id_and_cache(self):\n        if self._segments_by_recording_id is None:\n            from cytoolz import groupby\n            self._segments_by_recording_id = groupby(lambda seg: seg.recording_id, self)\n        return self._segments_by_recording_id\n\n    def __repr__(self) -> str:\n        return f'SupervisionSet(len={len(self)})'\n\n    def __getitem__(self, item: str) -> SupervisionSegment:\n        return self.segments[item]\n\n    def __iter__(self) -> Iterable[SupervisionSegment]:\n        return iter(self.segments.values())\n\n    def __len__(self) -> int:\n        return len(self.segments)\n\n    def __add__(self, other: 'SupervisionSet') -> 'SupervisionSet':\n        return SupervisionSet(segments={**self.segments, **other.segments})\n", "path": "lhotse/supervision.py"}]}