Repository: lhotse-speech/lhotse

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 ?



Discussion:
I’ll look into it. Recently I’ve been able to overcome many such rounding issues, but probably haven’t updated all parts of the library. 
Thanks! LMK If I can help.

Resolving patch:
diff --git a/lhotse/supervision.py b/lhotse/supervision.py
--- a/lhotse/supervision.py
+++ b/lhotse/supervision.py
@@ -194,7 +194,8 @@ def find(
             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 @@ def find(
             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 @@ def find(
             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.


Test patch: