Add causal person fusion and MuVAP stage-3 baselines
This commit is contained in:
+217
@@ -0,0 +1,217 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user