Skip to content

Commit 18f74bf

Browse files
committed
Support flat native NeMo lists in idxpacks
Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com>
1 parent 6c57e73 commit 18f74bf

3 files changed

Lines changed: 214 additions & 22 deletions

File tree

nemo/collections/common/data/lhotse/cutset.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,23 +1662,45 @@ def read_nemo_manifest(config) -> tuple[CutSet, bool]:
16621662
}
16631663
tar_kwargs_extra = {"indexed": indexed, **indexed_extra, **pack_extra} if indexed else {}
16641664
is_tarred = config.get("tarred_audio_filepaths") is not None
1665+
manifest_filepath = config.manifest_filepath
1666+
manifest_is_scalar = isinstance(manifest_filepath, (str, Path))
1667+
manifest_is_flat_list = (
1668+
isinstance(manifest_filepath, (list, tuple, ListConfig))
1669+
and bool(manifest_filepath)
1670+
and all(isinstance(item, (str, Path)) for item in manifest_filepath)
1671+
)
1672+
tarred_audio_filepaths = config.get("tarred_audio_filepaths")
1673+
tar_is_scalar = isinstance(tarred_audio_filepaths, (str, Path))
1674+
tar_is_flat_list = (
1675+
isinstance(tarred_audio_filepaths, (list, tuple, ListConfig))
1676+
and bool(tarred_audio_filepaths)
1677+
and all(isinstance(item, (str, Path)) for item in tarred_audio_filepaths)
1678+
)
16651679
if index_pack is not None:
1666-
if not isinstance(config.manifest_filepath, (str, Path)):
1680+
if not (manifest_is_scalar or manifest_is_flat_list):
16671681
raise ValueError(
16681682
"Packed native NeMo datasets require manifest_filepath to be "
1669-
"a string/Path (brace expansion is supported); list forms are not."
1683+
"a string/Path or a non-empty flat list of strings/Paths; nested "
1684+
"and weighted list forms are not supported."
16701685
)
1671-
if is_tarred and not isinstance(config.tarred_audio_filepaths, (str, Path)):
1686+
if is_tarred and not (tar_is_scalar or tar_is_flat_list):
16721687
raise ValueError(
16731688
"Packed native NeMo datasets require tarred_audio_filepaths to "
1674-
"be a string/Path (brace expansion is supported); list forms are not."
1689+
"be a string/Path or a non-empty flat list of strings/Paths; nested "
1690+
"list forms are not supported."
1691+
)
1692+
if is_tarred and manifest_is_flat_list != tar_is_flat_list:
1693+
raise ValueError(
1694+
"Packed native NeMo manifest_filepath and tarred_audio_filepaths "
1695+
"must both use scalar path specs or both use non-empty flat lists."
16751696
)
1676-
if isinstance(config.manifest_filepath, (str, Path)):
1697+
packed_flat_list = index_pack is not None and (manifest_is_flat_list or (is_tarred and tar_is_flat_list))
1698+
if manifest_is_scalar or packed_flat_list:
16771699
if is_tarred and not metadata_only:
16781700
cuts = CutSet(
16791701
LazyNeMoTarredIterator(
1680-
config.manifest_filepath,
1681-
tar_paths=config.tarred_audio_filepaths,
1702+
manifest_filepath,
1703+
tar_paths=tarred_audio_filepaths,
16821704
skip_missing_manifest_entries=config.get("skip_missing_manifest_entries", False),
16831705
slice_length=config.get("slice_length", None),
16841706
**tar_kwargs_extra,
@@ -1688,9 +1710,7 @@ def read_nemo_manifest(config) -> tuple[CutSet, bool]:
16881710
if not force_finite:
16891711
cuts = cuts.repeat(preserve_id=True)
16901712
else:
1691-
cuts = CutSet(
1692-
LazyNeMoIterator(config.manifest_filepath, **notar_kwargs, **notar_kwargs_extra, **common_kwargs)
1693-
)
1713+
cuts = CutSet(LazyNeMoIterator(manifest_filepath, **notar_kwargs, **notar_kwargs_extra, **common_kwargs))
16941714
else:
16951715
# Format option 1:
16961716
# Assume it's [[path1], [path2], ...] (same for tarred_audio_filepaths).

scripts/dataloading/convert_indexes_to_idxpack.py

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,25 @@ def _require_scalar_spec(value, field: str) -> None:
230230
)
231231

232232

233+
def _is_nonempty_flat_path_list(value) -> bool:
234+
return (
235+
isinstance(value, (list, tuple, ListConfig))
236+
and bool(value)
237+
and all(isinstance(item, (str, Path)) for item in value)
238+
)
239+
240+
241+
def _require_scalar_or_flat_path_list(value, field: str) -> None:
242+
if isinstance(value, (str, Path)):
243+
return
244+
if _is_nonempty_flat_path_list(value):
245+
return
246+
raise ValueError(
247+
f"Packed native NeMo {field} must be a string/Path or a non-empty flat "
248+
"list of strings/Paths; nested and weighted list forms are not supported."
249+
)
250+
251+
233252
def _shard_number(path: str) -> int | None:
234253
matches = re.findall(r"\d+", Path(path).stem)
235254
return int(matches[-1]) if matches else None
@@ -256,6 +275,29 @@ def _validate_native_pair(manifests: list[str], tars: list[str]) -> None:
256275
)
257276

258277

278+
def _expand_flat_native_pairs(manifest_specs, tar_specs) -> tuple[list[str], list[str]]:
279+
if len(manifest_specs) != len(tar_specs):
280+
raise ValueError(
281+
"Packed native NeMo flat lists require one tar path spec per "
282+
f"manifest path spec: manifests={len(manifest_specs)}, tars={len(tar_specs)}"
283+
)
284+
285+
manifests: list[str] = []
286+
tars: list[str] = []
287+
for position, (manifest_spec, tar_spec) in enumerate(zip(manifest_specs, tar_specs)):
288+
pair_manifests = _expand_jsonl(manifest_spec)
289+
pair_tars = _expand_tars(tar_spec)
290+
if len(pair_manifests) != len(pair_tars):
291+
raise ValueError(
292+
"Packed native NeMo flat lists require each positional manifest/tar "
293+
f"pair to expand to the same number of shards; position={position}, "
294+
f"manifests={len(pair_manifests)}, tars={len(pair_tars)}"
295+
)
296+
manifests.extend(pair_manifests)
297+
tars.extend(pair_tars)
298+
return manifests, tars
299+
300+
259301
def discover_pack_collections(
260302
entry,
261303
collections: Optional[list[IndexPackCollectionSpec]] = None,
@@ -305,25 +347,42 @@ def discover_pack_collections(
305347

306348
if typ in {"nemo", "nemo_tarred", "share_gpt", *_TRANSFORM_TYPES} and entry.get("manifest_filepath") is not None:
307349
raw = entry.get("manifest_filepath")
308-
_require_scalar_spec(raw, "manifest_filepath")
309-
manifests = _expand_jsonl(raw)
350+
if typ == "share_gpt":
351+
_require_scalar_spec(raw, "manifest_filepath")
352+
else:
353+
_require_scalar_or_flat_path_list(raw, "manifest_filepath")
354+
raw_tars = entry.get("tarred_audio_filepaths")
355+
if raw_tars is None:
356+
manifests = _expand_jsonl(raw)
357+
tars = None
358+
else:
359+
if typ == "share_gpt":
360+
raise NotImplementedError(
361+
"Packed ShareGPT supports JSONL manifests with direct/remote "
362+
"audio paths, not paired audio tar files."
363+
)
364+
_require_scalar_or_flat_path_list(raw_tars, "tarred_audio_filepaths")
365+
manifest_is_flat_list = _is_nonempty_flat_path_list(raw)
366+
tar_is_flat_list = _is_nonempty_flat_path_list(raw_tars)
367+
if manifest_is_flat_list != tar_is_flat_list:
368+
raise ValueError(
369+
"Packed native NeMo manifest_filepath and tarred_audio_filepaths "
370+
"must both use scalar path specs or both use non-empty flat lists."
371+
)
372+
if manifest_is_flat_list:
373+
manifests, tars = _expand_flat_native_pairs(raw, raw_tars)
374+
else:
375+
manifests = _expand_jsonl(raw)
376+
tars = _expand_tars(raw_tars)
377+
_validate_native_pair(manifests, tars)
310378
_add_collection(
311379
collections,
312380
role="manifest",
313381
kind=JSONL,
314382
source_spec=raw,
315383
paths=manifests,
316384
)
317-
if entry.get("tarred_audio_filepaths") is not None:
318-
if typ == "share_gpt":
319-
raise NotImplementedError(
320-
"Packed ShareGPT supports JSONL manifests with direct/remote "
321-
"audio paths, not paired audio tar files."
322-
)
323-
raw_tars = entry.get("tarred_audio_filepaths")
324-
_require_scalar_spec(raw_tars, "tarred_audio_filepaths")
325-
tars = _expand_tars(raw_tars)
326-
_validate_native_pair(manifests, tars)
385+
if tars is not None:
327386
_add_collection(
328387
collections,
329388
role="tar",

tests/collections/common/test_lhotse_index_pack.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
from click.testing import CliRunner
2222
from lhotse.index_pack import IndexPack, IndexPackCollectionSpec, index_pack_collection_key, write_index_pack
2323
from lhotse.indexing import create_jsonl_index
24+
from omegaconf import OmegaConf
2425
from scripts.dataloading import convert_indexes_to_idxpack as converter
2526
from scripts.dataloading.convert_indexes_to_idxpack import main
2627

2728
from nemo.collections.common.data.lhotse import nemo_adapters, text_adapters
29+
from nemo.collections.common.data.lhotse.cutset import read_nemo_manifest
2830
from nemo.collections.common.data.lhotse.indexed_adapters import create_tar_index as create_nemo_tar_index
2931
from nemo.collections.common.data.lhotse.nemo_adapters import LazyNeMoIterator, LazyNeMoTarredIterator
3032

@@ -146,6 +148,117 @@ def test_convert_input_cfg_sidecars_to_one_index_pack(tmp_path):
146148
assert pack.num_segments == 2
147149

148150

151+
def test_flat_native_lists_use_aggregate_pack_and_preserve_positional_pairs(tmp_path, monkeypatch):
152+
manifests = []
153+
declared_tar_paths = []
154+
expected_texts = []
155+
for position, (manifest_id, tar_id) in enumerate(((3076, 2), (4100, 0), (5200, 1))):
156+
manifest = tmp_path / f"manifest_{manifest_id}.jsonl"
157+
member = f"sample-{position}.wav"
158+
text = f"text-{position}"
159+
manifest.write_text(
160+
json.dumps(
161+
{
162+
"audio_filepath": member,
163+
"duration": 1.0,
164+
"text": text,
165+
"lang": "en",
166+
}
167+
)
168+
+ "\n"
169+
)
170+
create_jsonl_index(manifest)
171+
manifests.append(str(manifest))
172+
declared_tar_paths.append(f"ais://bucket/audio_{tar_id}.tar")
173+
expected_texts.append(text)
174+
175+
input_cfg = tmp_path / "flat-lists.yaml"
176+
input_cfg.write_text(
177+
yaml.safe_dump(
178+
{
179+
"type": "nemo_tarred",
180+
"manifest_filepath": manifests,
181+
"tarred_audio_filepaths": declared_tar_paths,
182+
}
183+
)
184+
)
185+
output = tmp_path / "flat-lists.idxpack"
186+
result = CliRunner().invoke(
187+
main,
188+
[
189+
"--output",
190+
str(output),
191+
"--native-tar-paths-only",
192+
str(input_cfg),
193+
],
194+
)
195+
assert result.exit_code == 0, result.output
196+
197+
with IndexPack(output) as pack:
198+
manifest_collection = pack.collection(index_pack_collection_key("manifest", "jsonl", manifests))
199+
tar_collection = pack.collection(index_pack_collection_key("tar", "nemo_tar", declared_tar_paths))
200+
assert manifest_collection.sequence_count == 3
201+
assert tar_collection.sequence_count == 3
202+
assert [tar_collection.path_for_shard(idx) for idx in range(3)] == declared_tar_paths
203+
204+
monkeypatch.setenv("USE_AIS_GET_BATCH", "true")
205+
config = OmegaConf.create(
206+
{
207+
"manifest_filepath": manifests,
208+
"tarred_audio_filepaths": declared_tar_paths,
209+
"indexed": True,
210+
"index_pack": str(output),
211+
"force_finite": True,
212+
}
213+
)
214+
cuts, is_tarred = read_nemo_manifest(config)
215+
cuts = list(cuts)
216+
assert is_tarred
217+
assert [cut.supervisions[0].text for cut in cuts] == expected_texts
218+
assert [cut.recording.sources[0].source for cut in cuts] == [
219+
f"{tar_path}/sample-{position}.wav" for position, tar_path in enumerate(declared_tar_paths)
220+
]
221+
222+
223+
def test_converter_rejects_mixed_scalar_and_flat_native_pairs(tmp_path):
224+
input_cfg = tmp_path / "mixed-path-forms.yaml"
225+
input_cfg.write_text(
226+
yaml.safe_dump(
227+
{
228+
"type": "nemo_tarred",
229+
"manifest_filepath": [str(tmp_path / "manifest.jsonl")],
230+
"tarred_audio_filepaths": str(tmp_path / "audio.tar"),
231+
}
232+
)
233+
)
234+
235+
result = CliRunner().invoke(
236+
main,
237+
["--dry-run", "--output", str(tmp_path / "mixed.idxpack"), str(input_cfg)],
238+
)
239+
assert result.exit_code != 0
240+
assert "must both use scalar path specs or both use non-empty flat lists" in str(result.exception)
241+
242+
243+
def test_converter_rejects_nested_native_manifest_lists(tmp_path):
244+
input_cfg = tmp_path / "nested-lists.yaml"
245+
input_cfg.write_text(
246+
yaml.safe_dump(
247+
{
248+
"type": "nemo",
249+
"manifest_filepath": [[str(tmp_path / "manifest.jsonl"), 0.5]],
250+
}
251+
)
252+
)
253+
254+
result = CliRunner().invoke(
255+
main,
256+
["--dry-run", "--output", str(tmp_path / "nested.idxpack"), str(input_cfg)],
257+
)
258+
assert result.exit_code != 0
259+
assert "nested and weighted list forms are not supported" in str(result.exception)
260+
261+
149262
def test_lazy_nemo_iterator_uses_pack_without_expanding_shards(tmp_path, monkeypatch):
150263
manifests = []
151264
source_spec = str(tmp_path / "manifest__OP_0..1_CL_.jsonl")

0 commit comments

Comments
 (0)