#!/usr/bin/env python3 """Merge two preference datasets by concatenating mmap, metadata, and info files. This module provides functionality to merge two preference datasets that were created by the make_dataset function. The merge operation concatenates: - Memory-mapped binary files (data_*.bin) - Metadata JSONL files (meta_*.jsonl) - Info JSON files (info_*.json) The merge is done by appending the second dataset to the first, updating all indices appropriately. """ import json import os from typing import Dict, List, Any, Tuple import numpy as np import tqdm from collections import defaultdict # Constants from the original file SEMANTIC_N_CODEBOOKS = 1 N_TOKENS_AUDIO = 25 * 8 * 60 # max 8 mins of audio def read_jsonl(filepath: str) -> List[Dict[str, Any]]: """Read JSONL file and return list of dictionaries. Args: filepath: Path to the JSONL file Returns: List of metadata dictionaries """ data = [] with open(filepath, 'r') as f: for line in f: data.append(json.loads(line.strip())) return data def write_jsonl(data: List[Dict[str, Any]], filepath: str, do_append: bool = False) -> None: """Write list of dictionaries to JSONL file. Args: data: List of metadata dictionaries filepath: Path to output JSONL file do_append: Whether to append to existing file """ mode = 'a' if do_append else 'w' with open(filepath, mode) as f: for item in data: f.write(json.dumps(item) + '\n') def read_json(filepath: str) -> Dict[str, Any]: """Read JSON file and return dictionary. Args: filepath: Path to the JSON file Returns: Dictionary containing info data """ with open(filepath, 'r') as f: return json.load(f) def write_json(data: Dict[str, Any], filepath: str) -> None: """Write dictionary to JSON file. Args: data: Dictionary to write filepath: Path to output JSON file """ with open(filepath, 'w') as f: json.dump(data, f, indent=2) def load_sample_from_mmap( mmap_path: str, sample_idx: int, t_data_memmap: int = N_TOKENS_AUDIO ) -> np.ndarray: """Load a single sample from the memory-mapped file. Args: mmap_path: Path to the mmap binary file sample_idx: Index of the sample to load t_data_memmap: Number of tokens per sample Returns: Array of shape (t_data_memmap, SEMANTIC_N_CODEBOOKS) """ sample_size = t_data_memmap * SEMANTIC_N_CODEBOOKS offset = sample_idx * sample_size mmap = np.memmap(mmap_path, dtype=np.uint16, mode='r') sample_data = mmap[offset:offset + sample_size].copy() del mmap return sample_data.reshape(t_data_memmap, SEMANTIC_N_CODEBOOKS) def merge_preference_datasets( input_dir1: str, input_dir2: str, output_dir: str, is_val: bool = False, t_data_memmap: int = N_TOKENS_AUDIO, validate: bool = True, ) -> None: """Merge two preference datasets by concatenating them. This function merges two preference datasets (created by make_dataset) by: 1. Concatenating the mmap binary files 2. Concatenating the metadata JSONL files 3. Merging the info JSON files with updated indices 4. Optionally validating the merged dataset Args: input_dir1: Path to the first dataset directory input_dir2: Path to the second dataset directory output_dir: Path to output directory for merged dataset is_val: Whether this is validation set (affects file naming) t_data_memmap: Number of tokens per sample in the mmap validate: Whether to validate the merge by checking samples Raises: ValueError: If input files don't exist or validation fails """ dset_type = "val" if is_val else "tr" # Define file paths mmap1_path = os.path.join(input_dir1, f"data_{dset_type}.bin") meta1_path = os.path.join(input_dir1, f"meta_{dset_type}.jsonl") info1_path = os.path.join(input_dir1, f"info_{dset_type}.json") mmap2_path = os.path.join(input_dir2, f"data_{dset_type}.bin") meta2_path = os.path.join(input_dir2, f"meta_{dset_type}.jsonl") info2_path = os.path.join(input_dir2, f"info_{dset_type}.json") output_mmap_path = os.path.join(output_dir, f"data_{dset_type}.bin") output_meta_path = os.path.join(output_dir, f"meta_{dset_type}.jsonl") output_info_path = os.path.join(output_dir, f"info_{dset_type}.json") # Validate input files exist for path in [mmap1_path, meta1_path, info1_path, mmap2_path, meta2_path, info2_path]: if not os.path.exists(path): raise ValueError(f"Input file does not exist: {path}") # Create output directory if it doesn't exist os.makedirs(output_dir, exist_ok=True) print(f"Merging {dset_type} datasets...") print(f" Input 1: {input_dir1}") print(f" Input 2: {input_dir2}") print(f" Output: {output_dir}") # Step 1: Load metadata to determine sample counts print("\n[1/4] Reading metadata files...") meta1 = read_jsonl(meta1_path) meta2 = read_jsonl(meta2_path) n_samples1 = len(meta1) n_samples2 = len(meta2) print(f" Dataset 1: {n_samples1:,} samples") print(f" Dataset 2: {n_samples2:,} samples") print(f" Total: {n_samples1 + n_samples2:,} samples") # Step 2: Concatenate mmap files print("\n[2/4] Concatenating mmap files...") sample_size = t_data_memmap * SEMANTIC_N_CODEBOOKS # Load dataset 1 mmap1 = np.memmap(mmap1_path, dtype=np.uint16, mode='r') size1 = n_samples1 * sample_size assert len(mmap1) == size1, f"Dataset 1 size mismatch: {len(mmap1)} != {size1}" # Load dataset 2 mmap2 = np.memmap(mmap2_path, dtype=np.uint16, mode='r') size2 = n_samples2 * sample_size assert len(mmap2) == size2, f"Dataset 2 size mismatch: {len(mmap2)} != {size2}" # Create output mmap and copy data total_size = size1 + size2 output_mmap = np.memmap(output_mmap_path, dtype=np.uint16, mode='w+', shape=(total_size,)) # Copy dataset 1 print(" Copying dataset 1...") output_mmap[:size1] = mmap1[:] # Copy dataset 2 print(" Copying dataset 2...") output_mmap[size1:total_size] = mmap2[:] output_mmap.flush() del output_mmap, mmap1, mmap2 print(f" Total mmap size: {total_size:,} elements ({total_size * 2 / 1024**3:.2f} GB)") # Step 3: Concatenate metadata files print("\n[3/4] Merging metadata files...") write_jsonl(meta1, output_meta_path, do_append=False) write_jsonl(meta2, output_meta_path, do_append=True) print(f" Wrote {n_samples1 + n_samples2:,} metadata entries") # Step 4: Merge info files with updated indices print("\n[4/4] Merging info files...") info1 = read_json(info1_path) info2 = read_json(info2_path) # Create merged info merged_info = defaultdict(dict) # Copy dataset 1 info as-is for dataset_name, dataset_info in info1.items(): merged_info[dataset_name] = dataset_info.copy() # Add dataset 2 info with offset indices for dataset_name, dataset_info in info2.items(): if dataset_name in merged_info: # Dataset exists in both - merge idx_list with offset if "idx_list" in dataset_info: offset_idx_list = [idx + n_samples1 for idx in dataset_info["idx_list"]] merged_info[dataset_name]["idx_list"].extend(offset_idx_list) else: # New dataset - add with offset merged_info[dataset_name] = dataset_info.copy() if "idx_list" in dataset_info: merged_info[dataset_name]["idx_list"] = [ idx + n_samples1 for idx in dataset_info["idx_list"] ] write_json(dict(merged_info), output_info_path) # Print summary print("\nMerge Summary:") for dataset_name, dataset_info in sorted(merged_info.items()): n_clips = len(dataset_info.get("idx_list", [])) print(f" {dataset_name}: {n_clips:,} clips") # Step 5: Validation if validate: print("\n[Validation] Checking merged dataset integrity...") validate_merged_dataset( output_mmap_path, output_meta_path, output_info_path, t_data_memmap, num_samples_to_check=min(10, n_samples1 + n_samples2), ) print("✓ Validation passed!") print("\n✅ Merge complete!") def validate_merged_dataset( mmap_path: str, meta_path: str, info_path: str, t_data_memmap: int = N_TOKENS_AUDIO, num_samples_to_check: int = 10, ) -> None: """Validate the merged dataset by checking sample integrity. This function validates that: 1. The number of metadata entries matches the mmap size 2. Info indices point to valid metadata entries 3. Sample data can be loaded correctly Args: mmap_path: Path to the mmap binary file meta_path: Path to the metadata JSONL file info_path: Path to the info JSON file t_data_memmap: Number of tokens per sample num_samples_to_check: Number of samples to check in detail Raises: AssertionError: If validation fails """ # Load metadata and info metadata = read_jsonl(meta_path) info = read_json(info_path) n_samples = len(metadata) sample_size = t_data_memmap * SEMANTIC_N_CODEBOOKS # Check mmap size mmap = np.memmap(mmap_path, dtype=np.uint16, mode='r') expected_size = n_samples * sample_size assert len(mmap) == expected_size, f"Size mismatch: {len(mmap)} != {expected_size}" del mmap print(f" ✓ Mmap size matches metadata count: {n_samples:,} samples") # Check info indices all_indices = set() for dataset_name, dataset_info in info.items(): if "idx_list" in dataset_info: idx_list = dataset_info["idx_list"] # Check indices are in valid range for idx in idx_list: assert 0 <= idx < n_samples, f"Invalid index {idx} for dataset {dataset_name}" all_indices.add(idx) print(f" ✓ All {len(all_indices):,} indices in info are valid") # Sample some entries and verify they can be loaded print(f" Checking {num_samples_to_check} random samples...") check_indices = np.random.choice( n_samples, size=min(num_samples_to_check, n_samples), replace=False ) for idx in tqdm.tqdm(check_indices, desc=" Validating samples"): # Load from mmap sample = load_sample_from_mmap(mmap_path, idx, t_data_memmap) assert sample.shape == (t_data_memmap, SEMANTIC_N_CODEBOOKS) # Check metadata exists meta = metadata[idx] assert "id" in meta, f"Metadata at index {idx} missing 'id' field" assert "dataset" in meta, f"Metadata at index {idx} missing 'dataset' field" print(f" ✓ All checked samples are valid") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( description="Merge two preference datasets by concatenating them" ) parser.add_argument( "--input_dir1", type=str, required=True, help="Path to the first dataset directory" ) parser.add_argument( "--input_dir2", type=str, required=True, help="Path to the second dataset directory" ) parser.add_argument( "--output_dir", type=str, required=True, help="Path to output directory for merged dataset" ) parser.add_argument( "--is_val", action="store_true", help="Whether this is validation set (default: False for train set)" ) parser.add_argument( "--t_data_memmap", type=int, default=N_TOKENS_AUDIO, help=f"Number of tokens per sample (default: {N_TOKENS_AUDIO})" ) parser.add_argument( "--no_validate", action="store_true", help="Skip validation step" ) args = parser.parse_args() merge_preference_datasets( input_dir1=args.input_dir1, input_dir2=args.input_dir2, output_dir=args.output_dir, is_val=args.is_val, t_data_memmap=args.t_data_memmap, validate=not args.no_validate, )