Files

218 lines
6.0 KiB
Python
Raw Permalink Normal View History

import argparse
import os
from collections import defaultdict
from dataclasses import dataclass
from typing import List
import numpy as np
import torch
from tqdm import tqdm
from config.setup import init_yaml_config
from data.utils import load_audio
from preprocess.vap.utils import (
EVENT_PADDING_SEC,
EVENT_SEG_LEN_SEC,
SAMPLE_RATE,
TEST_GROUPS,
VAL_GROUPS,
ProcessConfig,
event_crop,
get_dataset_split,
print_dataset_summary,
)
SEG_LEN_SEC: int = EVENT_SEG_LEN_SEC
PADDING_SEC: float = EVENT_PADDING_SEC
AUDIO_DIRS = {1: "audio", 2: "audio_stereo"}
@dataclass(slots=True)
class TurnEvent:
part: str
group: str
conv_id: str
region_start: float
region_end: float
prev_spk: str
next_spk: str
timing: str
gap_type: str
label: str
crop_start: float = 0.0
crop_end: float = 0.0
def parse_line(line: str) -> TurnEvent:
tokens = line.strip().split()
if len(tokens) != 10:
return None
entry = TurnEvent(
part=tokens[0],
group=tokens[1],
conv_id=tokens[2],
region_start=float(tokens[3]),
region_end=float(tokens[4]),
prev_spk=tokens[5],
next_spk=tokens[6],
timing=tokens[7],
gap_type=tokens[8],
label=tokens[9],
)
crop = event_crop(entry.timing, entry.region_start, entry.region_end)
if crop is None:
return None
entry.crop_start, entry.crop_end = crop
return entry
def group_by_conversation(entries: List[TurnEvent]):
grouped = defaultdict(list)
for entry in entries:
grouped[(entry.part, entry.group, entry.conv_id)].append(entry)
return dict(grouped)
def format_output_line(entry: TurnEvent):
location = f"{entry.part} {entry.group} {entry.conv_id} {entry.region_start:.2f} {entry.region_end:.2f}"
event = f"{entry.prev_spk} {entry.next_spk} {entry.timing} {entry.gap_type} {entry.label}"
return f"{location} {event}"
def load_entries_from_disk(cfg: ProcessConfig):
splits = {"train": [], "val": [], "test": []}
path = os.path.join(cfg.fisher_path, cfg.input_file)
with open(path, "r") as f:
parsed_entries = (parse_line(line) for line in f)
valid_entries = (e for e in parsed_entries if e is not None)
for entry in valid_entries:
split_name = get_dataset_split(entry.group, cfg)
if split_name in cfg.active_splits:
splits[split_name].append(entry)
return splits
def save_segment(entry: TurnEvent, audio: np.ndarray, cfg: ProcessConfig, channels: int):
seg_samples = int(SEG_LEN_SEC * SAMPLE_RATE)
start_sample = int(entry.crop_start * SAMPLE_RATE)
end_sample = start_sample + seg_samples
if end_sample > audio.shape[1]:
return False
seg = audio[:, start_sample:end_sample]
if seg.shape[1] != seg_samples:
return False
out_dir = os.path.join(
cfg.fisher_path,
entry.part,
"tune",
AUDIO_DIRS[channels],
entry.group,
entry.conv_id,
)
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, f"{entry.region_start:.2f}.npy")
if not os.path.exists(out_path):
pcm16 = (seg.clamp(-1.0, 1.0) * 32767).round().to(torch.int16)
np.save(out_path, pcm16.numpy())
return True
def process_split(
split_name: str, entries: List[TurnEvent], cfg: ProcessConfig, channels: int
):
if not entries:
return [], []
out_lines: List[str] = []
successful_events: List[TurnEvent] = []
grouped_convos = group_by_conversation(entries)
for (part, group, cid), convo_entries in tqdm(
grouped_convos.items(), desc=f"Processing {split_name}"
):
audio_sub_dir = "p1" if part == "p1" else "p2"
audio_path = os.path.join(
cfg.fisher_path, audio_sub_dir, "audio", group, f"{cid}.wav"
)
if not os.path.exists(audio_path):
continue
audio, _ = load_audio(audio_path, mono=channels == 1)
for entry in convo_entries:
if save_segment(entry, audio, cfg, channels):
out_lines.append(format_output_line(entry))
successful_events.append(entry)
return out_lines, successful_events
def run_pipeline(cfg: ProcessConfig, channels: int = 1):
splits = load_entries_from_disk(cfg)
all_saved_events = []
for split_name in cfg.active_splits:
entries = splits.get(split_name, [])
if not entries:
continue
out_lines, success_events = process_split(split_name, entries, cfg, channels)
all_saved_events.extend(success_events)
# The manifest lists which events have audio, so it is written once per
# channel layout and stays identical between them.
if out_lines:
out_path = os.path.join(cfg.fisher_path, f"{split_name}_events.txt")
with open(out_path, "w") as f:
f.write("\n".join(out_lines) + "\n")
if all_saved_events:
print_dataset_summary(all_saved_events)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="config/yaml/vap_role.yaml")
parser.add_argument(
"--splits",
nargs="+",
choices=("train", "val", "test"),
default=["val", "test"],
help="splits to cut clips for; training reads segments, not events, so "
"train clips are only needed to enlarge the probe's tuning pool",
)
parser.add_argument(
"--channels",
type=int,
choices=(1, 2),
default=1,
help="1 writes tune/audio for the mono setups, 2 writes tune/audio_stereo for original VAP",
)
args = parser.parse_args()
yaml_cfg = init_yaml_config(args.config)
config = ProcessConfig(
fisher_path=yaml_cfg["fisher_path"],
input_file="turn_events.txt",
val_groups=VAL_GROUPS,
test_groups=TEST_GROUPS,
active_splits=set(args.splits),
)
run_pipeline(config, channels=args.channels)