Skip to content

feat(audio-datasets): agent-ready initial-manifest stages (FLEURS, ReadSpeech)#2180

Open
shubhamNvidia wants to merge 3 commits into
NVIDIA-NeMo:mainfrom
shubhamNvidia:agent_pr/datasets
Open

feat(audio-datasets): agent-ready initial-manifest stages (FLEURS, ReadSpeech)#2180
shubhamNvidia wants to merge 3 commits into
NVIDIA-NeMo:mainfrom
shubhamNvidia:agent_pr/datasets

Conversation

@shubhamNvidia

Copy link
Copy Markdown
Contributor

Summary

Makes the dataset initial-manifest stages agent-ready. Additive only.

  • datasets/fleurs/create_initial_manifest.py, datasets/readspeech/create_initial_manifest.py — subclass AgentReady and add describe() / StageContract. These are source stages: they read a dataset layout and emit audio_filepath + metadata records, which the contract now declares. Manifest-creation logic unchanged.

Notes for reviewers

  • Additive, backward compatible.

Test plan

  • pytest tests/stages/audio/datasets -q

…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).
@shubhamNvidia
shubhamNvidia requested a review from a team as a code owner July 7, 2026 15:54
@shubhamNvidia
shubhamNvidia requested review from praateekmahajan and removed request for a team July 7, 2026 15:54
@copy-pr-bot

copy-pr-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires AgentReady + describe() / StageContract onto six audio stages and introduces two new supporting modules (_agent_ready.py, _residency.py). The changes are additive and the existing processing logic is untouched.

  • CreateInitialManifestReadSpeechStage.describe() declares only filepath_key and text_key in writes, but collect_audio_files also emits sample_rate, book_id, and reader_id into every AudioTask — the contract is incomplete for agent-based chaining.
  • _agent_ready.py introduces AgentReady, StageContract, IOSpec, and related dataclasses; describe_static() imports from a non-existent _agent_registry module (flagged in a prior review thread).
  • _residency.py adds audio-residency helpers; _as_soundfile_array's channel heuristic can silently corrupt very short clips (flagged in a prior thread).

Confidence Score: 4/5

Safe 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

Filename Overview
nemo_curator/stages/audio/datasets/readspeech/create_initial_manifest.py describe() contract is incomplete: collect_audio_files also writes sample_rate, book_id, and reader_id into every AudioTask, but these keys are absent from the StageContract writes field.
nemo_curator/stages/audio/_agent_ready.py New file: defines AgentReady mixin, StageContract, IOSpec, ParamSpec, and serialization helpers. describe_static() always raises ImportError (missing _agent_registry.py), and set iteration in _jsonable_default is non-deterministic — both flagged in previous threads.
nemo_curator/stages/audio/_residency.py New file: provides resolve_audio, resolve_audio_path, and _as_soundfile_array helpers. Channel-heuristic in _as_soundfile_array silently corrupts short-clip WAVs — flagged in previous thread.
nemo_curator/stages/audio/common.py Four stages wired with AgentReady + describe(). ManifestReader.describe() omits writes (flagged in prior thread); other contracts appear correct for their respective key sets.
nemo_curator/stages/audio/datasets/fleurs/create_initial_manifest.py describe() contract correctly declares filepath_key and text_key, which are the only keys process_transcript writes into AudioTask data.

Reviews (2): Last reviewed commit: "fix(audio): match _residency.py to found..." | Re-trigger Greptile

Comment on lines +301 to +313
@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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +76 to +81
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +211 to +214
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]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Comment on lines +252 to +253
def describe(self) -> StageContract:
return StageContract(cardinality="1:N fan-out", wrappable=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +72 to +77
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),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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).

Suggested change
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),
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant