197 lines
5.7 KiB
Python
197 lines
5.7 KiB
Python
"""Cut Fisher into fixed segments of stereo audio beside their raw VAD.
|
|
|
|
Class labels are *not* baked in here. Each segment stores the two-speaker VAD
|
|
padded with `--context-sec` of extra frames on both sides, which is everything
|
|
`ProjectionWindow` needs to produce role-based or speaker-based labels in the
|
|
dataloader. Switching projection setups therefore never requires re-running
|
|
this script.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
from dataclasses import dataclass
|
|
from glob import glob
|
|
|
|
import numpy as np
|
|
from tqdm import tqdm
|
|
|
|
from config.setup import init_yaml_config
|
|
from data.utils import load_audio, load_fisher_vad
|
|
from preprocess.vap.utils import (
|
|
SAMPLE_RATE,
|
|
TEST_GROUPS,
|
|
VAL_GROUPS,
|
|
ProcessConfig,
|
|
get_dataset_split,
|
|
)
|
|
|
|
SEG_SEC: int = 30
|
|
OVERLAP: int = 10
|
|
VAD_HZ: int = 25
|
|
CONTEXT_SEC: float = 2.0
|
|
|
|
STRIDE_SEC: int = SEG_SEC - OVERLAP
|
|
SEG_SAMPLES: int = int(SEG_SEC * SAMPLE_RATE)
|
|
STRIDE_SAMPLES: int = int(STRIDE_SEC * SAMPLE_RATE)
|
|
SEG_VAD_FRAMES: int = int(SEG_SEC * VAD_HZ)
|
|
|
|
|
|
@dataclass()
|
|
class SegmentMeta:
|
|
part: str
|
|
group: str
|
|
cid: str
|
|
seg_id: int
|
|
start_t: float
|
|
end_t: float
|
|
nframes: int
|
|
|
|
|
|
def format_meta(s: SegmentMeta) -> str:
|
|
return f"{s.part} {s.group} {s.cid} {s.seg_id} {s.start_t:.2f} {s.end_t:.2f} {s.nframes}\n"
|
|
|
|
|
|
def extract_and_save_chunks(
|
|
audio: np.ndarray,
|
|
vad: np.ndarray,
|
|
part: str,
|
|
group: str,
|
|
cid: str,
|
|
cfg: ProcessConfig,
|
|
context_frames: int,
|
|
):
|
|
total_samples = audio.shape[-1]
|
|
total_vad_frames = vad.shape[-1]
|
|
|
|
audio_out = os.path.join(cfg.fisher_path, part, "seg", "audio", group, cid)
|
|
vad_out = os.path.join(cfg.fisher_path, part, "seg", "vad", group, cid)
|
|
os.makedirs(audio_out, exist_ok=True)
|
|
os.makedirs(vad_out, exist_ok=True)
|
|
|
|
segments = []
|
|
start_sample = 0
|
|
seg_idx = 0
|
|
|
|
while start_sample + SEG_SAMPLES <= total_samples:
|
|
end_sample = start_sample + SEG_SAMPLES
|
|
|
|
chunk_audio = audio[:, start_sample:end_sample]
|
|
pcm16 = np.round(np.clip(chunk_audio, -1.0, 1.0) * 32767).astype(np.int16)
|
|
audio_file = os.path.join(audio_out, f"{seg_idx}.npy")
|
|
if not os.path.exists(audio_file):
|
|
np.save(audio_file, pcm16)
|
|
|
|
curr_vad_start = int((start_sample / SAMPLE_RATE) * VAD_HZ)
|
|
curr_vad_end = curr_vad_start + SEG_VAD_FRAMES
|
|
|
|
ext_start = max(0, curr_vad_start - context_frames)
|
|
ext_end = min(total_vad_frames, curr_vad_end + context_frames)
|
|
|
|
chunk_vad = vad[:, ext_start:ext_end]
|
|
|
|
pad_left = max(0, context_frames - curr_vad_start)
|
|
pad_right = max(0, (curr_vad_end + context_frames) - total_vad_frames)
|
|
|
|
if pad_left > 0 or pad_right > 0:
|
|
chunk_vad = np.pad(
|
|
chunk_vad, ((0, 0), (pad_left, pad_right)), mode="constant"
|
|
)
|
|
|
|
vad_file = os.path.join(vad_out, f"{seg_idx}.npy")
|
|
if not os.path.exists(vad_file):
|
|
np.save(vad_file, chunk_vad.astype(np.float32))
|
|
|
|
segments.append(
|
|
SegmentMeta(
|
|
part=part,
|
|
group=group,
|
|
cid=cid,
|
|
seg_id=seg_idx,
|
|
start_t=start_sample / SAMPLE_RATE,
|
|
end_t=end_sample / SAMPLE_RATE,
|
|
nframes=total_vad_frames,
|
|
)
|
|
)
|
|
|
|
start_sample += STRIDE_SAMPLES
|
|
seg_idx += 1
|
|
|
|
return segments
|
|
|
|
|
|
def process_entry(
|
|
part: str,
|
|
group: str,
|
|
cid: str,
|
|
cfg: ProcessConfig,
|
|
context_frames: int,
|
|
):
|
|
audio_path = os.path.join(cfg.fisher_path, part, "audio", group, f"{cid}.wav")
|
|
vad_dir = os.path.join(cfg.fisher_path, part, "regions_vap", group)
|
|
|
|
if not os.path.exists(audio_path):
|
|
return []
|
|
|
|
# Both channels are kept: the speaker-based setup needs one per speaker and
|
|
# the mono setups downmix in the dataloader.
|
|
audio_tensor, duration = load_audio(audio_path, mono=False)
|
|
audio = audio_tensor.numpy()
|
|
|
|
nframes = int(round(duration * VAD_HZ))
|
|
vad_tensor = load_fisher_vad(vad_dir, cid, nframes)
|
|
vad = vad_tensor.numpy()
|
|
|
|
return extract_and_save_chunks(audio, vad, part, group, cid, cfg, context_frames)
|
|
|
|
|
|
def run_pipeline(cfg: ProcessConfig, context_frames: int):
|
|
pattern = os.path.join(cfg.fisher_path, "*", "regions_vap", "*", "*_words.txt")
|
|
all_files = glob(pattern)
|
|
|
|
entries = {"train": [], "val": [], "test": []}
|
|
seen = set()
|
|
|
|
for vad_file in tqdm(all_files, desc="Building Dataset"):
|
|
parts = vad_file.split(os.sep)
|
|
part, group = parts[-4], parts[-2]
|
|
cid = "_".join(os.path.basename(vad_file).split("_")[:3])
|
|
|
|
if (part, group, cid) in seen:
|
|
continue
|
|
seen.add((part, group, cid))
|
|
|
|
split = get_dataset_split(group, cfg)
|
|
if split not in cfg.active_splits:
|
|
continue
|
|
|
|
segments = process_entry(part, group, cid, cfg, context_frames)
|
|
entries[split].extend(format_meta(s) for s in segments)
|
|
|
|
for split in cfg.active_splits:
|
|
lines = entries.get(split, [])
|
|
if lines:
|
|
out_file = os.path.join(cfg.fisher_path, f"{split}.txt")
|
|
with open(out_file, "w") as f:
|
|
f.writelines(lines)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--config", default="config/yaml/vap_role.yaml")
|
|
parser.add_argument(
|
|
"--context-sec",
|
|
type=float,
|
|
default=CONTEXT_SEC,
|
|
help="VAD kept on each side of a segment; must cover the widest projection window",
|
|
)
|
|
args = parser.parse_args()
|
|
yaml_cfg = init_yaml_config(args.config)
|
|
|
|
config = ProcessConfig(
|
|
fisher_path=yaml_cfg["fisher_path"],
|
|
val_groups=VAL_GROUPS,
|
|
test_groups=TEST_GROUPS,
|
|
active_splits={"train", "val", "test"},
|
|
)
|
|
run_pipeline(config, int(round(args.context_sec * VAD_HZ)))
|