call-qa-processing / ml-services /src /data /combined_emotion_dataset.py
aniketqxp's picture
feat: add processing service runtime
f1ef7e2
Raw
History Blame Contribute Delete
8.1 kB
"""
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()