Repository: lhotse-speech/lhotse

Issue:
Combine and split lazily opened manifest
I am trying to do the following:

```python
cs = load_manifest_lazy('data/manifests/cuts_dev_raw.jsonl.gz')
cs = combine([cs, cs.perturb_speed(0.9)])
cs_splits = cs.split_lazy(output_dir='tmp', chunk_size=1000)
```

However, this produces the following error:
```
  File "/export/c07/draj/mini_scale_2022/lhotse/lhotse/cut.py", line 3594, in split_lazy
    return split_manifest_lazy(self, output_dir=output_dir, chunk_size=chunk_size)
  File "/export/c07/draj/mini_scale_2022/lhotse/lhotse/utils.py", line 302, in split_manifest_lazy
    writer.write(item)
  File "/export/c07/draj/mini_scale_2022/lhotse/lhotse/serialization.py", line 187, in write
    print(json.dumps(manifest.to_dict()), file=self.file)
AttributeError: 'str' object has no attribute 'to_dict'
```

On further investigation, the `split_lazy` seems to be working fine on either `cs` or the perturbed version of `cs`, and the error only happens on the combined manifest.


Discussion:
I can fix it in a week or so when I’m back, or feel free to give it shot if it’s blocking you. 
I'll give it a shot. I just wanted to make sure if it's actually a bug or just incorrect usage on my end.

Resolving patch:
diff --git a/lhotse/dataset/sampling/dynamic.py b/lhotse/dataset/sampling/dynamic.py
--- a/lhotse/dataset/sampling/dynamic.py
+++ b/lhotse/dataset/sampling/dynamic.py
@@ -88,7 +88,7 @@ def __init__(
         :param seed: Random seed used to consistently shuffle the dataset across different processes.
         """
         super().__init__(world_size=world_size, rank=rank, seed=seed)
-        if not all(cs.is_lazy for cs in cuts):
+        if not all(cs.is_lazy for cs in cuts if isinstance(cs, CutSet)):
             warnings.warn(
                 "You are using DynamicCutSampler with an eagerly read CutSet. "
                 "You won't see any memory/speed benefits with that setup. "
diff --git a/lhotse/dataset/sampling/dynamic_bucketing.py b/lhotse/dataset/sampling/dynamic_bucketing.py
--- a/lhotse/dataset/sampling/dynamic_bucketing.py
+++ b/lhotse/dataset/sampling/dynamic_bucketing.py
@@ -109,7 +109,7 @@ def __init__(
         :param seed: Random seed used to consistently shuffle the dataset across different processes.
         """
         super().__init__(world_size=world_size, rank=rank, seed=seed)
-        if not all(cs.is_lazy for cs in cuts):
+        if not all(cs.is_lazy for cs in cuts if isinstance(cs, CutSet)):
             warnings.warn(
                 "You are using DynamicBucketingSampler with an eagerly read CutSet. "
                 "You won't see any memory/speed benefits with that setup. "
diff --git a/lhotse/serialization.py b/lhotse/serialization.py
--- a/lhotse/serialization.py
+++ b/lhotse/serialization.py
@@ -541,6 +541,8 @@ class LazyIteratorChain:
     """
     A thin wrapper over multiple iterators that enables to combine lazy manifests
     in Lhotse. It iterates all underlying iterables sequentially.
+
+    .. note:: if any of the input iterables is a dict, we'll iterate only its values.
     """
 
     def __init__(self, *iterators: Iterable) -> None:
@@ -554,7 +556,10 @@ def __init__(self, *iterators: Iterable) -> None:
                 self.iterators.append(it)
 
     def __iter__(self):
-        return (item for it in self.iterators for item in it)
+        for it in self.iterators:
+            if isinstance(it, dict):
+                it = it.values()
+            yield from it
 
     def values(self):
         yield from self


Test patch:
diff --git a/test/known_issues/test_lazy_cuts_issues.py b/test/known_issues/test_lazy_cuts_issues.py
new file mode 100644
--- /dev/null
+++ b/test/known_issues/test_lazy_cuts_issues.py
@@ -0,0 +1,16 @@
+from tempfile import NamedTemporaryFile, TemporaryDirectory
+
+from lhotse import CutSet, combine, load_manifest_lazy
+from lhotse.testing.dummies import DummyManifest
+
+
+def test_lazy_cuts_combine_split_issue():
+    # Test for lack of exception
+    cuts = DummyManifest(CutSet, begin_id=0, end_id=1000)
+    with TemporaryDirectory() as d, NamedTemporaryFile(suffix=".jsonl.gz") as f:
+        cuts.to_file(f.name)
+        f.flush()
+
+        cuts_lazy = load_manifest_lazy(f.name)
+        cuts_lazy = combine(cuts_lazy, cuts_lazy.perturb_speed(0.9))
+        cuts_lazy.split_lazy(d, chunk_size=100)