Spaces:
Running on Zero
Running on Zero
| """ | |
| Combined CREMA-D + RAVDESS metadata preparation. | |
| This script combines the already-processed CREMA-D and RAVDESS metadata files | |
| into one unified metadata CSV for Model V2 training. | |
| Model V1: | |
| CREMA-D only | |
| Model V2: | |
| CREMA-D + RAVDESS | |
| Why: | |
| CREMA-D gives a strong baseline, while RAVDESS adds more labeled emotional | |
| speech data. Combining them can improve generalization and reduce confusion | |
| between emotions such as sadness/fear and happy/anger. | |
| Run from ml-services: | |
| python -m src.data.combined_emotion_dataset | |
| """ | |
| import json | |
| from pathlib import Path | |
| from typing import Dict, List | |
| import pandas as pd | |
| PROJECT_ROOT = Path(__file__).resolve().parents[3] | |
| ML_SERVICES_ROOT = PROJECT_ROOT / "ml-services" | |
| PROCESSED_DIR = ML_SERVICES_ROOT / "data" / "processed" | |
| CREMAD_METADATA_PATH = PROCESSED_DIR / "cremad_metadata.csv" | |
| RAVDESS_METADATA_PATH = PROCESSED_DIR / "ravdess_metadata.csv" | |
| COMBINED_METADATA_PATH = PROCESSED_DIR / "combined_emotion_metadata.csv" | |
| COMBINED_SUMMARY_PATH = PROCESSED_DIR / "combined_emotion_summary.json" | |
| REQUIRED_COLUMNS = [ | |
| "file_path", | |
| "filename", | |
| "dataset", | |
| "actor_id", | |
| "emotion_label", | |
| "sentiment_label", | |
| "is_negative", | |
| "split", | |
| ] | |
| EXPECTED_EMOTIONS = {"anger", "disgust", "fear", "happy", "neutral", "sadness"} | |
| EXPECTED_SPLITS = {"train", "validation", "test"} | |
| def load_metadata(path: Path, dataset_name: str) -> pd.DataFrame: | |
| """ | |
| Load metadata CSV and validate basic existence. | |
| """ | |
| if not path.exists(): | |
| raise FileNotFoundError( | |
| f"Missing {dataset_name} metadata: {path}\n" | |
| "Run the dataset metadata preparation script first." | |
| ) | |
| dataframe = pd.read_csv(path) | |
| if dataframe.empty: | |
| raise ValueError(f"{dataset_name} metadata is empty: {path}") | |
| return dataframe | |
| def ensure_dataset_column(dataframe: pd.DataFrame, dataset_name: str) -> pd.DataFrame: | |
| """ | |
| Ensure a dataset column exists. | |
| Older CREMA-D metadata may not include a dataset column, so we add it here. | |
| """ | |
| dataframe = dataframe.copy() | |
| if "dataset" not in dataframe.columns: | |
| dataframe["dataset"] = dataset_name | |
| dataframe["dataset"] = dataframe["dataset"].fillna(dataset_name) | |
| return dataframe | |
| def standardize_metadata_columns( | |
| dataframe: pd.DataFrame, | |
| dataset_name: str, | |
| ) -> pd.DataFrame: | |
| """ | |
| Keep only the common columns needed for combined training. | |
| Extra dataset-specific columns are intentionally not included in the combined | |
| metadata to keep the training pipeline simple. | |
| """ | |
| dataframe = ensure_dataset_column(dataframe, dataset_name) | |
| missing_columns = [ | |
| column for column in REQUIRED_COLUMNS if column not in dataframe.columns | |
| ] | |
| if missing_columns: | |
| raise ValueError( | |
| f"{dataset_name} metadata is missing required columns: {missing_columns}" | |
| ) | |
| standardized = dataframe[REQUIRED_COLUMNS].copy() | |
| standardized["dataset"] = dataset_name | |
| # Make actor IDs globally unique across datasets. | |
| # Example: | |
| # CREMAD_1001 | |
| # RAVDESS_1 | |
| standardized["source_actor_id"] = standardized["actor_id"].astype(str) | |
| standardized["global_actor_id"] = ( | |
| standardized["dataset"].astype(str) | |
| + "_" | |
| + standardized["actor_id"].astype(str) | |
| ) | |
| return standardized | |
| def validate_combined_metadata(dataframe: pd.DataFrame) -> None: | |
| """ | |
| Validate combined metadata quality. | |
| """ | |
| missing_emotions = EXPECTED_EMOTIONS - set(dataframe["emotion_label"].unique()) | |
| unexpected_emotions = set(dataframe["emotion_label"].unique()) - EXPECTED_EMOTIONS | |
| if missing_emotions: | |
| raise ValueError(f"Missing expected emotions: {missing_emotions}") | |
| if unexpected_emotions: | |
| raise ValueError(f"Unexpected emotion labels: {unexpected_emotions}") | |
| actual_splits = set(dataframe["split"].unique()) | |
| if actual_splits != EXPECTED_SPLITS: | |
| raise ValueError(f"Unexpected split labels: {actual_splits}") | |
| missing_paths = [ | |
| path for path in dataframe["file_path"].tolist() if not Path(path).exists() | |
| ] | |
| if missing_paths: | |
| raise FileNotFoundError( | |
| "Some audio paths in combined metadata do not exist. " | |
| f"First missing path: {missing_paths[0]}" | |
| ) | |
| duplicate_rows = dataframe.duplicated(subset=["file_path"]).sum() | |
| if duplicate_rows > 0: | |
| raise ValueError(f"Found duplicate file paths: {duplicate_rows}") | |
| def create_combined_summary(dataframe: pd.DataFrame) -> Dict: | |
| """ | |
| Create summary statistics for combined CREMA-D + RAVDESS metadata. | |
| """ | |
| summary = { | |
| "dataset": "CREMA-D + RAVDESS", | |
| "total_records": int(len(dataframe)), | |
| "datasets": dataframe["dataset"].value_counts().to_dict(), | |
| "splits": dataframe["split"].value_counts().to_dict(), | |
| "split_by_dataset": dataframe.groupby(["dataset", "split"]) | |
| .size() | |
| .unstack(fill_value=0) | |
| .to_dict(), | |
| "emotion_labels": dataframe["emotion_label"].value_counts().to_dict(), | |
| "emotion_by_dataset": dataframe.groupby(["dataset", "emotion_label"]) | |
| .size() | |
| .unstack(fill_value=0) | |
| .to_dict(), | |
| "sentiment_labels": dataframe["sentiment_label"].value_counts().to_dict(), | |
| "sentiment_by_dataset": dataframe.groupby(["dataset", "sentiment_label"]) | |
| .size() | |
| .unstack(fill_value=0) | |
| .to_dict(), | |
| "global_actors_per_split": dataframe.groupby("split")["global_actor_id"] | |
| .nunique() | |
| .to_dict(), | |
| "actors_by_dataset": dataframe.groupby("dataset")["global_actor_id"] | |
| .nunique() | |
| .to_dict(), | |
| "notes": [ | |
| "Combined metadata uses the existing speaker-aware split from each dataset.", | |
| "global_actor_id prevents actor ID collisions between CREMA-D and RAVDESS.", | |
| "RAVDESS calm is mapped to neutral.", | |
| "RAVDESS surprised samples are excluded.", | |
| "This metadata is prepared for Model V2 training.", | |
| ], | |
| } | |
| return summary | |
| def build_combined_metadata() -> pd.DataFrame: | |
| """ | |
| Build combined CREMA-D + RAVDESS metadata. | |
| """ | |
| cremad_df = load_metadata(CREMAD_METADATA_PATH, "CREMA-D") | |
| ravdess_df = load_metadata(RAVDESS_METADATA_PATH, "RAVDESS") | |
| cremad_standardized = standardize_metadata_columns(cremad_df, "CREMA-D") | |
| ravdess_standardized = standardize_metadata_columns(ravdess_df, "RAVDESS") | |
| combined_df = pd.concat( | |
| [cremad_standardized, ravdess_standardized], | |
| ignore_index=True, | |
| ) | |
| combined_df = combined_df.sort_values( | |
| by=["split", "dataset", "emotion_label", "filename"] | |
| ).reset_index(drop=True) | |
| validate_combined_metadata(combined_df) | |
| return combined_df | |
| def save_combined_outputs(dataframe: pd.DataFrame, summary: Dict) -> None: | |
| """ | |
| Save combined metadata and summary. | |
| """ | |
| PROCESSED_DIR.mkdir(parents=True, exist_ok=True) | |
| dataframe.to_csv(COMBINED_METADATA_PATH, index=False) | |
| with COMBINED_SUMMARY_PATH.open("w", encoding="utf-8") as file: | |
| json.dump(summary, file, indent=2) | |
| def main() -> None: | |
| combined_df = build_combined_metadata() | |
| summary = create_combined_summary(combined_df) | |
| save_combined_outputs(combined_df, summary) | |
| print("\nCombined CREMA-D + RAVDESS metadata preparation completed successfully.") | |
| print("-" * 80) | |
| print(f"Total records: {summary['total_records']}") | |
| print(f"Dataset distribution: {summary['datasets']}") | |
| print(f"Split distribution: {summary['splits']}") | |
| print(f"Emotion distribution: {summary['emotion_labels']}") | |
| print(f"Sentiment distribution: {summary['sentiment_labels']}") | |
| print(f"Actors by dataset: {summary['actors_by_dataset']}") | |
| print(f"Global actors per split: {summary['global_actors_per_split']}") | |
| print("-" * 80) | |
| print(f"Saved metadata CSV to: {COMBINED_METADATA_PATH}") | |
| print(f"Saved summary JSON to: {COMBINED_SUMMARY_PATH}") | |
| if __name__ == "__main__": | |
| main() |