feat(audio-datasets): agent-ready initial-manifest stages (FLEURS, ReadSpeech)#2180
feat(audio-datasets): agent-ready initial-manifest stages (FLEURS, ReadSpeech)#2180shubhamNvidia wants to merge 3 commits into
Conversation
…idency resolver Shared base imported by the audio stage modules to make them agent-ready: - _agent_ready.py: AgentReady mixin, StageContract/IOSpec/Gates/Role - _residency.py: input residency resolver (file/waveform/auto) - common.py: agent-ready helpers (ensure_mono/ensure_waveform_2d) Excludes the agent orchestration layer (registry/catalog/conformance/planning/agent).
…eContract + residency)
Greptile SummaryThis PR wires
Confidence Score: 4/5Safe to merge with a small fix: the ReadSpeech contract omits three keys the stage actually produces, which breaks agent-based chaining for any downstream stage that reads sample_rate, book_id, or reader_id. The ReadSpeech stage writes sample_rate, book_id, and reader_id into every AudioTask but its describe() contract declares only filepath_key and text_key. For a PR whose explicit goal is agent-ready contracts, an incomplete contract is a present defect. All other contracts and the existing processing logic are correct and unmodified. nemo_curator/stages/audio/datasets/readspeech/create_initial_manifest.py — the describe() writes spec needs the three additional keys that collect_audio_files always emits. Important Files Changed
Reviews (2): Last reviewed commit: "fix(audio): match _residency.py to found..." | Re-trigger Greptile |
| @classmethod | ||
| def describe_static(cls) -> StageContract: | ||
| """Instance-free contract for discovery/planning. | ||
|
|
||
| Uses class defaults + ``AGENT_STATIC`` and never runs ``__init__`` side | ||
| effects, so it is safe for stages with required constructor args. Prefer | ||
| the instance-level :meth:`describe` (or | ||
| ``_agent_registry.build_contract``) when resolved key *values* are | ||
| needed. | ||
| """ | ||
| from nemo_curator.stages.audio._agent_registry import static_contract | ||
|
|
||
| return static_contract(cls) |
There was a problem hiding this comment.
describe_static() always raises ImportError
describe_static() lazily imports static_contract from nemo_curator.stages.audio._agent_registry, but _agent_registry.py does not exist in the repository — it is the only file referenced from _agent_ready.py that is absent. Any call to AgentReady.describe_static() (e.g., CreateInitialManifestFleursStage.describe_static()) will immediately raise ImportError: cannot import name 'static_contract' from 'nemo_curator.stages.audio._agent_registry', making the entire static-discovery path broken. The instance-level describe() is unaffected, but this makes the describe_static / AGENT_STATIC class-variable machinery dead code right now.
| if getattr(waveform, "ndim", 0) == 2: # noqa: PLR2004 - 2 == a (channels, samples) 2-D array | ||
| channels, samples = waveform.shape | ||
| if channels == 1: | ||
| return waveform[0] | ||
| if channels < samples: | ||
| return waveform.T |
There was a problem hiding this comment.
Channel-heuristic fails for very short audio clips
The heuristic channels < samples used to detect whether the waveform is stored in (channels, samples) vs (samples, channels) layout breaks for any clip shorter than the channel count. For example, a 2-channel, 1-sample waveform has channels=2, samples=1; 2 < 1 is False, so the transpose is skipped and the array is passed to sf.write in the wrong orientation, producing corrupted output. In practice audio clips are long, but this could silently corrupt temp WAVs generated from synthetic or test fixtures with very few samples.
| if value is None or isinstance(value, (str, int, float, bool)): | ||
| return value | ||
| if isinstance(value, (list, tuple, set)): | ||
| return [_jsonable_default(v) for v in value] |
There was a problem hiding this comment.
set iteration in _jsonable_default is non-deterministic
When a ParamSpec.default or choices contains a set, _jsonable_default iterates it and returns a list, but sets are unordered in CPython 3.7+, so repeated calls may produce different orderings. Downstream consumers that compare serialized contracts (e.g., caching or hashing) may see spurious differences. Sorting the elements before building the list (or converting to a sorted list at the point where set defaults are declared) would make the output stable.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| def describe(self) -> StageContract: | ||
| return StageContract(cardinality="1:N fan-out", wrappable=False) |
There was a problem hiding this comment.
ManifestReader.describe() omits the audio_filepath write from its contract
ManifestReader decomposes into FilePartitioningStage + ManifestReaderStage, and the inner ManifestReaderStage.describe() correctly advertises writes=IOSpec(data_keys=["audio_filepath"], ...). But ManifestReader.describe() returns only StageContract(cardinality="1:N fan-out", wrappable=False) with no writes declared. An agent consuming the composite stage's contract won't know that audio_filepath is produced, so it cannot chain a downstream stage that reads that key using the static-discovery path. Since wrappable=False prevents wrapping, the agent must fall back to inspecting the decomposed stages manually.
| def describe(self) -> StageContract: | ||
| return StageContract( | ||
| writes=IOSpec(data_keys=[self.filepath_key, self.text_key], produces=["disk"]), | ||
| cardinality="1:N fan-out", | ||
| gates=Gates(writes_to_disk=True, requires_internet_first_run=self.auto_download), | ||
| ) |
There was a problem hiding this comment.
The
writes spec omits sample_rate, book_id, and reader_id, which collect_audio_files unconditionally writes into every emitted AudioTask. An agent consuming the contract cannot discover these produced keys and will be unable to chain downstream stages that depend on them (e.g. resampling stages that read sample_rate).
| def describe(self) -> StageContract: | |
| return StageContract( | |
| writes=IOSpec(data_keys=[self.filepath_key, self.text_key], produces=["disk"]), | |
| cardinality="1:N fan-out", | |
| gates=Gates(writes_to_disk=True, requires_internet_first_run=self.auto_download), | |
| ) | |
| def describe(self) -> StageContract: | |
| return StageContract( | |
| writes=IOSpec( | |
| data_keys=[self.filepath_key, self.text_key, "sample_rate", "book_id", "reader_id"], | |
| produces=["disk"], | |
| ), | |
| cardinality="1:N fan-out", | |
| gates=Gates(writes_to_disk=True, requires_internet_first_run=self.auto_download), | |
| ) |
…pts/reads helpers)
Summary
Makes the dataset initial-manifest stages agent-ready. Additive only.
datasets/fleurs/create_initial_manifest.py,datasets/readspeech/create_initial_manifest.py— subclassAgentReadyand adddescribe()/StageContract. These are source stages: they read a dataset layout and emitaudio_filepath+ metadata records, which the contract now declares. Manifest-creation logic unchanged.Notes for reviewers
Test plan
pytest tests/stages/audio/datasets -q