| |
| """ |
| Dataset Validator - Validates the generated dataset for training. |
| |
| Checks: |
| 1. Structure: list of dicts with required fields |
| 2. Whisper features: shape [seq_len, 1280] |
| 3. SNAC tokens: multiple of 7, valid range |
| 4. Optional fields: text, answer, text_tokens, word_alignments |
| 5. Statistics and distribution |
| """ |
|
|
| import argparse |
| import sys |
| from pathlib import Path |
| from collections import defaultdict |
|
|
| import torch |
| import numpy as np |
|
|
|
|
| |
| SNAC_BASE = 128266 |
| SNAC_LAYERS = 7 |
| SNAC_VOCAB_PER_LAYER = 4096 |
| WHISPER_DIM = 1280 |
|
|
|
|
| def validate_sample(idx: int, sample: dict, verbose: bool = False) -> tuple[bool, list[str]]: |
| """Validate a single sample. Returns (is_valid, list of errors).""" |
| errors = [] |
| warnings = [] |
|
|
| |
| if "whisper_features" not in sample: |
| errors.append("Missing 'whisper_features'") |
| if "snac_tokens" not in sample: |
| errors.append("Missing 'snac_tokens'") |
|
|
| if errors: |
| return False, errors |
|
|
| |
| wf = sample["whisper_features"] |
| if not isinstance(wf, torch.Tensor): |
| errors.append(f"whisper_features should be Tensor, got {type(wf).__name__}") |
| else: |
| if wf.dim() != 2: |
| errors.append(f"whisper_features should be 2D [seq_len, 1280], got {wf.dim()}D") |
| elif wf.shape[1] != WHISPER_DIM: |
| errors.append(f"whisper_features dim should be {WHISPER_DIM}, got {wf.shape[1]}") |
| if wf.shape[0] == 0: |
| errors.append("whisper_features has 0 length") |
| if torch.isnan(wf).any(): |
| errors.append("whisper_features contains NaN values") |
| if torch.isinf(wf).any(): |
| errors.append("whisper_features contains Inf values") |
|
|
| |
| st = sample["snac_tokens"] |
| if not isinstance(st, torch.Tensor): |
| errors.append(f"snac_tokens should be Tensor, got {type(st).__name__}") |
| else: |
| if st.dim() != 1: |
| errors.append(f"snac_tokens should be 1D, got {st.dim()}D") |
| if len(st) == 0: |
| errors.append("snac_tokens has 0 length") |
| elif len(st) % SNAC_LAYERS != 0: |
| errors.append(f"snac_tokens length ({len(st)}) not multiple of {SNAC_LAYERS}") |
|
|
| |
| if len(st) > 0: |
| min_tok = st.min().item() |
| max_tok = st.max().item() |
| |
| if max_tok < SNAC_BASE: |
| |
| if min_tok < 0 or max_tok >= SNAC_VOCAB_PER_LAYER: |
| warnings.append(f"snac_tokens range [{min_tok}, {max_tok}] outside [0, {SNAC_VOCAB_PER_LAYER-1}]") |
| else: |
| |
| expected_max = SNAC_BASE + (SNAC_LAYERS * SNAC_VOCAB_PER_LAYER) |
| if min_tok < SNAC_BASE or max_tok >= expected_max: |
| warnings.append(f"snac_tokens with offset range [{min_tok}, {max_tok}] unexpected") |
|
|
| |
| if "text" in sample and not isinstance(sample["text"], str): |
| warnings.append(f"text should be str, got {type(sample['text']).__name__}") |
|
|
| if "answer" in sample and not isinstance(sample["answer"], str): |
| warnings.append(f"answer should be str, got {type(sample['answer']).__name__}") |
|
|
| if "text_tokens" in sample: |
| tt = sample["text_tokens"] |
| if not isinstance(tt, torch.Tensor): |
| warnings.append(f"text_tokens should be Tensor, got {type(tt).__name__}") |
| elif tt.dim() != 1: |
| warnings.append(f"text_tokens should be 1D, got {tt.dim()}D") |
|
|
| if "word_alignments" in sample: |
| wa = sample["word_alignments"] |
| if not isinstance(wa, list): |
| warnings.append(f"word_alignments should be list, got {type(wa).__name__}") |
|
|
| if verbose and warnings: |
| for w in warnings: |
| print(f" [WARN] Sample {idx}: {w}") |
|
|
| return len(errors) == 0, errors |
|
|
|
|
| def compute_statistics(dataset: list) -> dict: |
| """Compute dataset statistics.""" |
| stats = { |
| "total_samples": len(dataset), |
| "whisper_lengths": [], |
| "snac_lengths": [], |
| "snac_frames": [], |
| "has_text": 0, |
| "has_answer": 0, |
| "has_text_tokens": 0, |
| "has_word_alignments": 0, |
| "text_lengths": [], |
| "answer_lengths": [], |
| } |
|
|
| for sample in dataset: |
| if "whisper_features" in sample and isinstance(sample["whisper_features"], torch.Tensor): |
| stats["whisper_lengths"].append(sample["whisper_features"].shape[0]) |
|
|
| if "snac_tokens" in sample and isinstance(sample["snac_tokens"], torch.Tensor): |
| length = len(sample["snac_tokens"]) |
| stats["snac_lengths"].append(length) |
| stats["snac_frames"].append(length // SNAC_LAYERS) |
|
|
| if "text" in sample: |
| stats["has_text"] += 1 |
| if isinstance(sample["text"], str): |
| stats["text_lengths"].append(len(sample["text"])) |
|
|
| if "answer" in sample: |
| stats["has_answer"] += 1 |
| if isinstance(sample["answer"], str): |
| stats["answer_lengths"].append(len(sample["answer"])) |
|
|
| if "text_tokens" in sample: |
| stats["has_text_tokens"] += 1 |
|
|
| if "word_alignments" in sample: |
| stats["has_word_alignments"] += 1 |
|
|
| return stats |
|
|
|
|
| def print_statistics(stats: dict): |
| """Print dataset statistics.""" |
| print("\n" + "=" * 60) |
| print("DATASET STATISTICS") |
| print("=" * 60) |
|
|
| print(f"\nTotal samples: {stats['total_samples']}") |
|
|
| |
| if stats["whisper_lengths"]: |
| wl = np.array(stats["whisper_lengths"]) |
| print(f"\nWhisper features length:") |
| print(f" Min: {wl.min()}, Max: {wl.max()}, Mean: {wl.mean():.1f}, Std: {wl.std():.1f}") |
|
|
| |
| if stats["snac_lengths"]: |
| sl = np.array(stats["snac_lengths"]) |
| sf = np.array(stats["snac_frames"]) |
| print(f"\nSNAC tokens:") |
| print(f" Tokens - Min: {sl.min()}, Max: {sl.max()}, Mean: {sl.mean():.1f}") |
| print(f" Frames - Min: {sf.min()}, Max: {sf.max()}, Mean: {sf.mean():.1f}") |
|
|
| |
| duration_sec = sf * 0.0213 |
| print(f" Duration - Min: {duration_sec.min():.1f}s, Max: {duration_sec.max():.1f}s, Mean: {duration_sec.mean():.1f}s") |
|
|
| |
| print(f"\nOptional fields present:") |
| print(f" text: {stats['has_text']}/{stats['total_samples']} ({100*stats['has_text']/stats['total_samples']:.1f}%)") |
| print(f" answer: {stats['has_answer']}/{stats['total_samples']} ({100*stats['has_answer']/stats['total_samples']:.1f}%)") |
| print(f" text_tokens: {stats['has_text_tokens']}/{stats['total_samples']} ({100*stats['has_text_tokens']/stats['total_samples']:.1f}%)") |
| print(f" word_alignments: {stats['has_word_alignments']}/{stats['total_samples']} ({100*stats['has_word_alignments']/stats['total_samples']:.1f}%)") |
|
|
| |
| if stats["text_lengths"]: |
| tl = np.array(stats["text_lengths"]) |
| print(f"\nText (question) length (chars):") |
| print(f" Min: {tl.min()}, Max: {tl.max()}, Mean: {tl.mean():.1f}") |
|
|
| if stats["answer_lengths"]: |
| al = np.array(stats["answer_lengths"]) |
| print(f"\nAnswer length (chars):") |
| print(f" Min: {al.min()}, Max: {al.max()}, Mean: {al.mean():.1f}") |
|
|
|
|
| def validate_dataset(path: str, max_samples: int = None, verbose: bool = False) -> bool: |
| """Validate the dataset file.""" |
| print(f"\nValidating: {path}") |
| print("=" * 60) |
|
|
| |
| if not Path(path).exists(): |
| print(f"[ERROR] File not found: {path}") |
| return False |
|
|
| |
| print("Loading dataset...") |
| try: |
| dataset = torch.load(path, map_location="cpu", weights_only=False) |
| except Exception as e: |
| print(f"[ERROR] Failed to load dataset: {e}") |
| return False |
|
|
| |
| if not isinstance(dataset, list): |
| print(f"[ERROR] Dataset should be a list, got {type(dataset).__name__}") |
| return False |
|
|
| total = len(dataset) |
| print(f"Loaded {total} samples") |
|
|
| if total == 0: |
| print("[ERROR] Dataset is empty") |
| return False |
|
|
| |
| if max_samples and max_samples < total: |
| print(f"Validating first {max_samples} samples...") |
| samples_to_check = dataset[:max_samples] |
| else: |
| print(f"Validating all {total} samples...") |
| samples_to_check = dataset |
|
|
| valid_count = 0 |
| error_counts = defaultdict(int) |
|
|
| for idx, sample in enumerate(samples_to_check): |
| if not isinstance(sample, dict): |
| print(f"[ERROR] Sample {idx}: should be dict, got {type(sample).__name__}") |
| error_counts["not_dict"] += 1 |
| continue |
|
|
| is_valid, errors = validate_sample(idx, sample, verbose=verbose) |
| if is_valid: |
| valid_count += 1 |
| else: |
| for err in errors: |
| error_counts[err] += 1 |
| if verbose: |
| print(f"[ERROR] Sample {idx}: {err}") |
|
|
| |
| checked = len(samples_to_check) |
| invalid = checked - valid_count |
|
|
| print(f"\n{'=' * 60}") |
| print("VALIDATION SUMMARY") |
| print("=" * 60) |
| print(f"Samples checked: {checked}/{total}") |
| print(f"Valid: {valid_count} ({100*valid_count/checked:.1f}%)") |
| print(f"Invalid: {invalid} ({100*invalid/checked:.1f}%)") |
|
|
| if error_counts: |
| print(f"\nError breakdown:") |
| for err, count in sorted(error_counts.items(), key=lambda x: -x[1]): |
| print(f" {count:5d}x {err}") |
|
|
| |
| stats = compute_statistics(samples_to_check) |
| print_statistics(stats) |
|
|
| |
| if verbose and valid_count > 0: |
| print(f"\n{'=' * 60}") |
| print("SAMPLE INSPECTION (first valid sample)") |
| print("=" * 60) |
| for idx, sample in enumerate(samples_to_check): |
| is_valid, _ = validate_sample(idx, sample) |
| if is_valid: |
| print(f"Sample {idx}:") |
| for key, value in sample.items(): |
| if isinstance(value, torch.Tensor): |
| print(f" {key}: Tensor {value.shape} {value.dtype}") |
| elif isinstance(value, str): |
| preview = value[:100] + "..." if len(value) > 100 else value |
| print(f" {key}: '{preview}'") |
| elif isinstance(value, list): |
| print(f" {key}: list[{len(value)}]") |
| else: |
| print(f" {key}: {type(value).__name__}") |
| break |
|
|
| print("\n" + "=" * 60) |
| if invalid == 0: |
| print("RESULT: PASSED - All samples valid") |
| return True |
| else: |
| print(f"RESULT: FAILED - {invalid} invalid samples") |
| return False |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Validate dataset for training") |
| parser.add_argument("--path", type=str, required=True, help="Path to dataset .pt file") |
| parser.add_argument("--max-samples", type=int, default=None, help="Max samples to validate (default: all)") |
| parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") |
| args = parser.parse_args() |
|
|
| success = validate_dataset(args.path, args.max_samples, args.verbose) |
| sys.exit(0 if success else 1) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|