This document explains how to build ChainBench-ADD end to end using the repository’s five-stage pipeline.
The construction pipeline is intentionally staged so that speaker/text control, spoof generation, and post-generation delivery stay separated:
| Stage | Goal | Main input | Main output |
|---|---|---|---|
| Stage 1 | Curate source speech and assign speaker-disjoint splits | Raw corpora | data/stage1_source_curation/manifests/clean_real_manifest.csv |
| Stage 2 | Build standardized clean masters | Stage 1 manifest | data/stage2_clean_masters/manifests/clean_parent_manifest.csv |
| Stage 3 | Generate spoof clean parents | Stage 2 manifest | data/stage3_spoof_generation/manifests/clean_parent_manifest_all.csv |
| Stage 4 | Render delivery chains | Stage 3 combined manifest | data/stage4_delivery_chain/manifests/delivered_manifest.csv |
| Stage 5 | Validate, annotate, and package the release | Stage 4 manifest | data/ChainBench/metadata.csv and exported audio |
The design mirrors the paper: first fix transcript/speaker conditions, then synthesize spoof parents, then apply post-generation delivery so detector changes can be attributed to delivery rather than uncontrolled upstream variation.
The default config expects:
- English from Common Voice 24.0
- Mandarin Chinese from AISHELL-3
Adjust the dataset_root fields in config/stage1.json if your local layout differs.
The orchestration environment needs:
- Python 3.11+
ffmpeg/ffprobeconda- the package itself installed with
pip install -e .
Stage 3 additionally expects generator repos and conda environments named in config/stage3.json. Stage 4 uses pyroomacoustics for RIR simulation by default, with a synthetic fallback.
All default configs are repo-relative. If you want to keep data elsewhere, export:
export CHAINBENCH_ROOT=/abs/path/to/ChainBench-ADDStage 1 scans the raw corpora, normalizes transcripts, probes candidate audio, filters low-quality examples, selects speakers, and assigns speaker-disjoint train/dev/test splits.
chainbench/stage1 performs four main jobs:
-
Load corpus metadata
load_aishell_candidates(...)load_common_voice_candidates(...)
-
Normalize transcripts
- English keeps tokenized text with at least 4 tokens and rejects numeric-only strings.
- AISHELL-3 collapses tokenized Chinese text and rejects too-short/numeric-only strings.
-
Screen audio quality
- duration bounds
- mean/max volume checks
- speech ratio estimated via
silencedetect - optional cap on the number of audio checks per speaker
-
Select speakers and split them
- assign benchmark speaker IDs
- speaker-level split assignment via
assign_splits(...) - create a curated raw layout using symlinks
config/stage1.json controls:
- speaker targets per language
- utterances per speaker
- minimum utterances required to keep a speaker
- audio filtering thresholds
- train/dev/test ratios
- random seed
chainbench stage1 --config config/stage1.jsonUseful flags:
chainbench stage1 --config config/stage1.json --language en
chainbench stage1 --config config/stage1.json --log-level DEBUGMain outputs under data/stage1_source_curation/:
manifests/clean_real_manifest.csvmanifests/clean_real_manifest_en.csvmanifests/clean_real_manifest_zh.csvmanifests/en_selected_speakers.jsonmanifests/zh_selected_speakers.jsonmanifests/en_speaker_logs.jsonmanifests/zh_speaker_logs.jsonmanifests/stage1_summary.jsonraw/...symlinked source audio in curated split/speaker layout
Every row emitted here is still bona fide and still linked to a single curated speaker and transcript. No spoofing or delivery edits have happened yet.
Stage 2 turns the curated Stage 1 audio into standardized clean parents.
chainbench/stage2 reads the Stage 1 manifest and applies an FFmpeg pipeline built by build_filter_chain(...):
- optional silence trimming
- downmix to mono
- resample to 16 kHz
- loudness normalization
- PCM 16-bit WAV encoding
It validates every rendered file against the configured output spec and duration bounds.
config/stage2.json controls:
- Stage 1 manifest path
- output root
- worker count
- FFmpeg/FFprobe timeouts
- audio output format
- trim settings
- loudness normalization settings
- post-render validation bounds
chainbench stage2 --config config/stage2.jsonUseful flags:
chainbench stage2 --config config/stage2.json --language zh
chainbench stage2 --config config/stage2.json --limit 1000Main outputs under data/stage2_clean_masters/:
audio/...clean master WAVsmanifests/clean_parent_manifest.csvmanifests/clean_parent_manifest_en.csvmanifests/clean_parent_manifest_zh.csvmanifests/stage2_failures.jsonmanifests/stage2_summary.json
Rows remain one-to-one with curated bona fide parents, but now have a canonical clean-master audio path in clean_parent_path.
Stage 3 creates matched spoof clean parents from the Stage 2 bona fide clean parents.
Stage 3 deliberately separates generator choice from later delivery rendering.
For each clean parent, it:
- selects a fixed number of generators (
generators_per_parent, default 2) - chooses a same-speaker but different-utterance prompt/reference audio when possible
- materializes generator-specific job JSONL files
- launches one internal worker per generator batch inside the configured conda env
- optionally postprocesses raw generator outputs into standardized WAV
- validates spoof outputs
- merges bona fide clean parents and valid spoof clean parents into a combined manifest for Stage 4
config/stage3.json includes six generator adapters:
qwen3_tts_basecosyvoice3spark_ttsf5_ttsindex_tts2voxcpm
Each entry declares:
- repo path
- conda env
- supported languages
- adapter name
- adapter-specific model/runtime args
chainbench stage3 --config config/stage3.jsonUseful variants:
chainbench stage3 --config config/stage3.json --plan-only
chainbench stage3 --config config/stage3.json --only-generator qwen3_tts_base
chainbench stage3 --config config/stage3.json --language en --generators-per-parent 1Main outputs under data/stage3_spoof_generation/:
jobs/*.jsonlper-generator job filesjobs/*.adapter_config.jsonresults/*.jsonlper-generator result logslogs/*.logaudio_raw/...raw generator outputsaudio/...postprocessed spoof clean parentsmanifests/spoof_clean_manifest.csvmanifests/clean_parent_manifest_all.csv(Stage 4 input)manifests/stage3_failures.jsonmanifests/stage3_summary.json
After Stage 3 you have:
- original bona fide clean parents from Stage 2
- spoof clean parents generated from them
Both still live in the clean-parent regime. Delivery has not started yet.
Stage 4 is where ChainBench-ADD becomes a delivery-aware benchmark. It samples delivery templates, instantiates operator parameters, renders realized chains, and writes delivery metadata.
Stage 4 reads clean_parent_manifest_all.csv, so it renders delivery variants for both bona fide and spoof clean parents.
For each parent and selected family, it:
- samples up to
variants_per_parentrealized variants - concretizes template specs using parameter pools
- applies operators sequentially in waveform space
- optionally writes trace JSON
- standardizes final output when needed
- emits a delivered-manifest row with operator sequence and sampled params
The default config/stage4.json defines:
direct— 1 templateplatform_like— 6 templatestelephony— 10 templatessimreplay— 7 templateshybrid— 9 templates
That is 33 templates total, consistent with the paper.
Stage 4 implements reusable operators under chainbench/stage4/operators/:
resamplebandlimitcodecreencodepacket_lossnoiserircall_path
Notable implementation details:
- codec/re-encode use actual encode/decode round trips
- packet loss supports rate, burst length, and concealment mode
- RIR defaults to
pyroomacousticswith a synthetic fallback - final outputs are standardized to 16 kHz mono PCM WAV
Some families include paired template groups that deliberately preserve the same operator multiset while changing order. These are what make order-swap style analyses possible downstream.
chainbench stage4 --config config/stage4.jsonUseful variants:
chainbench stage4 --config config/stage4.json --plan-only
chainbench stage4 --config config/stage4.json --families telephony hybrid
chainbench stage4 --config config/stage4.json --language en --limit 500Main outputs under data/stage4_delivery_chain/:
audio/<family>/...delivered WAVstraces/<family>/...optional render tracesjobs/stage4_job_plan.jsonmanifests/delivered_manifest.csvmanifests/delivered_manifest_<family>.csvmanifests/delivered_manifest_<language>.csvmanifests/stage4_failures.jsonmanifests/stage4_summary.json
Each row now represents a realized delivery child linked back to its clean parent through parent_id, clean_parent_path, operator_seq, and operator_params.
Stage 5 validates the Stage 4 outputs, exports the final dataset layout, and adds the structural metadata used by the benchmark tasks.
Stage 5 performs three steps:
-
Validate audio
- duration
- sample rate / channels / codec
- NaN/Inf checks
- peak/clipping checks
- duration ratio relative to the clean parent
-
Export packaged audio
- copies files into the final release layout:
train/audio/<language>/<label>/...dev/audio/<language>/<label>/...test/audio/<language>/<label>/...
- copies files into the final release layout:
-
Annotate structural metadata
- operator substitution groups
- parameter perturbation groups
- order swap groups
- path groups / path step indices
- operator multiset keys
- path endpoint keys
These annotations are generated by annotate_structural_group_fields(...) and are what later drive the evaluation tasks.
chainbench stage5 --config config/stage5.jsonUseful variants:
chainbench stage5 --config config/stage5.json --skip-validation
chainbench stage5 --config config/stage5.json --language zhMain outputs under data/ChainBench/:
audio/...split audio treesmetadata.csvtrain/metadata.csvdev/metadata.csvtest/metadata.csvmanifest/stage5_failures.jsonmanifest/dataset_summary.jsonmanifest/stats_label_language_split.csvmanifest/stats_chain_family_label.csvmanifest/stats_generator_family_label.csv
The root metadata.csv includes release-critical fields such as:
- identifiers:
sample_id,parent_id,speaker_id,utterance_id - benchmark labels:
label,split_standard,language - provenance:
source_corpus,generator_family,generator_name - delivery structure:
chain_family,chain_template_id,chain_variant_index - operator fields:
operator_seq,operator_params,operator_multiset_key - task annotations:
operator_substitution_group_idparameter_perturbation_group_idorder_swap_group_idpath_group_idpath_step_index
chainbench stage1 --config config/stage1.json
chainbench stage2 --config config/stage2.json
chainbench stage3 --config config/stage3.json
chainbench stage4 --config config/stage4.json
chainbench stage5 --config config/stage5.jsonIf you want to inspect plans before heavy stages:
chainbench stage3 --config config/stage3.json --plan-only
chainbench stage4 --config config/stage4.json --plan-only- source corpus roots
- target speaker counts
- utterances per speaker
- audio thresholds
- enabled generators
- conda env names
- generators per parent
- model/checkpoint paths
- selected families
variants_per_parent- parameter pools
- RIR backend and fallback
- worker count
- output dataset root
- validation thresholds
- required family coverage list
A healthy run should leave you with:
data/ChainBench/metadata.csv- split-level metadata files under
train/,dev/, andtest/ - exported audio under split/language/label folders
manifest/dataset_summary.jsonshowing non-zero dataset rows- speaker-disjoint splits and structural group fields populated for downstream evaluation
If those are present, the benchmark is ready for chainbench eval.