{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "59534836",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"5\"\n",
    "import json\n",
    "import torch\n",
    "import numpy as np\n",
    "from tqdm import tqdm\n",
    "from suno_utils.tasks.ear_v3 import load_checkpoint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "216853bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "#base_dir = \"/app2/suno/data/christian/outputs/v3-base-data-ctx-t1\"\n",
    "base_dir = \"/app2/suno/data/christian/outputs/v3-base-data-ctx-rs-t4/\"\n",
    "dirs = os.listdir(base_dir)\n",
    "print(len(dirs))\n",
    "\n",
    "model_name = \"4n_25hz_2b_flow_5e5_sft_t8_500k\"\n",
    "CODEC_SCALE_FACTOR = 0.4"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97a9006b",
   "metadata": {},
   "outputs": [],
   "source": [
    "#ear_model_filepath = \"/app2/suno/checkpoints/2025-09-05_20-07-23_s6954/last_ckpt.pt\"\n",
    "#ear_model_filepath = \"/app2/suno/checkpoints/2025-09-18_10-32-07_s4862/best_ckpt.pt\"\n",
    "ear_model_filepath = \"/app2/suno/checkpoints/2025-10-08_11-52-17_s8263/best_ckpt.pt\"\n",
    "\n",
    "# s3://suno-data/christian/checkpoints/ear/\n",
    "\n",
    "ear_model = load_checkpoint(ear_model_filepath, device=\"cuda\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6df1e3b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from joblib import Parallel, delayed\n",
    "\n",
    "def load_vaes_for_dir(dirname):\n",
    "    outputs = []\n",
    "    for n in range(10):\n",
    "        first_vae_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_{n}_upsampled_vae.npz\")\n",
    "        if os.path.exists(first_vae_file):\n",
    "            first_vae = np.load(first_vae_file)[\"vae_latents\"]\n",
    "            outputs.append((n, first_vae))\n",
    "    return outputs\n",
    "\n",
    "def score_vae(vae_latents: np.ndarray):\n",
    "    first_vae_batch = torch.from_numpy(vae_latents).unsqueeze(0).float().cuda()\n",
    "\n",
    "    # lets crop the batches to multiple of 750s\n",
    "    first_vae_batch = first_vae_batch[:, :750 * (first_vae_batch.shape[1] // 750), :]\n",
    "\n",
    "    # fold both vaes into a batch with chunk size 750 seq_len\n",
    "    first_vae_batch = first_vae_batch.unfold(1, 750, 750).squeeze(0).permute(0, 2, 1)\n",
    "\n",
    "    with torch.no_grad():\n",
    "        first_scores = ear_model(first_vae_batch * CODEC_SCALE_FACTOR)\n",
    "\n",
    "    return first_scores.cpu().numpy()\n",
    "\n",
    "# Parallel load all vae latents\n",
    "#vae_data = Parallel(n_jobs=1)(\n",
    "#    delayed(load_vaes_for_dir)(dirname) for dirname in tqdm(dirs)\n",
    "#)\n",
    "# Filter out None\n",
    "#vae_data = [item for item in vae_data if item is not None]\n",
    "\n",
    "results = {}\n",
    "\n",
    "# Process in batches\n",
    "#for i in tqdm(range(len(vae_data))):\n",
    "#    clip_id, vae_latents = vae_data[i]\n",
    "\n",
    "for dirname in tqdm(dirs):\n",
    "\n",
    "    vae_data = load_vaes_for_dir(dirname)\n",
    "\n",
    "    for n, first_vae in vae_data:\n",
    "        first_vae_batch = torch.from_numpy(first_vae).unsqueeze(0).float().cuda()\n",
    "\n",
    "        # lets crop the batches to multiple of 750s\n",
    "        first_vae_batch = first_vae_batch[:, :750 * (first_vae_batch.shape[1] // 750), :]\n",
    "\n",
    "        # fold both vaes into a batch with chunk size 750 seq_len\n",
    "        first_vae_batch = first_vae_batch.unfold(1, 750, 750).squeeze(0).permute(0, 2, 1)\n",
    "\n",
    "        with torch.no_grad():\n",
    "            first_scores = ear_model(first_vae_batch * CODEC_SCALE_FACTOR)\n",
    "\n",
    "        first_scores = first_scores.cpu().numpy()\n",
    "\n",
    "        # extract the scores, first chunk, last chunk, and mean of chunks\n",
    "        first_chunk_scores = first_scores[0]\n",
    "        last_chunk_scores = first_scores[-1]\n",
    "        mean_chunk_scores = first_scores.mean()\n",
    "\n",
    "        results[dirname] = {\n",
    "            n : {\n",
    "                \"first_chunk\": first_chunk_scores,\n",
    "                \"last_chunk\": last_chunk_scores,\n",
    "                \"mean\": mean_chunk_scores\n",
    "            },\n",
    "        }\n",
    "    break\n",
    "\n",
    "# save the results\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c0fc040e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# given a clip_id (dirname) get all the vaes in there and score them, then listen to the best and worst ones\n",
    "\n",
    "vae_score_dict = {}\n",
    "vae_delta_dict = {}\n",
    "\n",
    "for dirname in tqdm(dirs[:1000]):\n",
    "    vae_data = load_vaes_for_dir(dirname)\n",
    "    if len(vae_data) != 2:\n",
    "        continue\n",
    "\n",
    "    for n, vae_latents in vae_data:\n",
    "        scores = score_vae(vae_latents)\n",
    "        first_score = scores[0]\n",
    "        last_score = scores[-1]\n",
    "        mean_score = scores.mean()\n",
    "\n",
    "        # Use a tuple key (dirname, n) to uniquely identify each vae\n",
    "        vae_score_dict[(dirname, n)] = {\n",
    "            \"dirname\": dirname,\n",
    "            \"vae_index\": n,\n",
    "            \"vae_latents\": vae_latents,\n",
    "            \"scores\": scores,\n",
    "            \"first_chunk\": first_score,\n",
    "            \"last_chunk\": last_score,\n",
    "            \"mean\": mean_score\n",
    "        }\n",
    "\n",
    "    first_chunk_delta = vae_score_dict[(dirname, 1)][\"first_chunk\"] - vae_score_dict[(dirname, 0)][\"first_chunk\"]\n",
    "    vae_delta_dict[dirname] = {\n",
    "        \"first_chunk_delta\": first_chunk_delta,\n",
    "        \"pos_first_chunk\": vae_score_dict[(dirname, 1)][\"first_chunk\"],\n",
    "        \"neg_first_chunk\": vae_score_dict[(dirname, 0)][\"first_chunk\"],\n",
    "        \"neg_vae_latents\": vae_score_dict[(dirname, 0)][\"vae_latents\"],\n",
    "        \"pos_vae_latents\": vae_score_dict[(dirname, 1)][\"vae_latents\"]\n",
    "    }\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e02e449a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# plot a histogram of the first chunk scores\n",
    "import matplotlib.pyplot as plt\n",
    "plt.hist([item[\"first_chunk_delta\"] for item in vae_delta_dict.values()], bins=100)\n",
    "plt.show()\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5380f91b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# sort the vae_score_dict by the mean\n",
    "#sorted_vae_score_dict = sorted(vae_score_dict.items(), key=lambda x: x[1][\"first_chunk\"], reverse=False)\n",
    "sorted_vae_delta_dict = sorted(vae_delta_dict.items(), key=lambda x: x[1][\"first_chunk_delta\"], reverse=True)\n",
    "\n",
    "# get the top 10 and bottom 10\n",
    "top_5 = sorted_vae_delta_dict[0:5]\n",
    "for item in top_5:\n",
    "    dirname, item_dict = item\n",
    "    # print the score\n",
    "    print(item_dict[\"first_chunk_delta\"])\n",
    "    print(\"neg\", item_dict[\"neg_first_chunk\"])\n",
    "    first_audio = codec_decode(item_dict[\"neg_vae_latents\"]).play()\n",
    "    print(\"pos\", item_dict[\"pos_first_chunk\"])\n",
    "    second_audio = codec_decode(item_dict[\"pos_vae_latents\"]).play()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1602fa4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# test some of the local data\n",
    "CODEC_FILEPATH = \"s3://suno-data/minz/models/dac_vae_tuned_25hz.pth\"\n",
    "\n",
    "from suno_utils.tasks.dac_vae_fixed_25hz import (\n",
    "    preload_models as preload_codec_models,\n",
    "    decode as codec_decode,\n",
    "    encode as codec_encode,\n",
    "    decode_stream_to_full_audio,\n",
    ") \n",
    "\n",
    "_ = preload_codec_models(CODEC_FILEPATH)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "32466272",
   "metadata": {},
   "outputs": [],
   "source": [
    "features = [\"stereo_width_delta\", \"total_delta\", \"ear_v3_score_first\", \"shimmer_score\"]\n",
    "\n",
    "def listen_to_vae(dirname):\n",
    "    first_vae_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_0_upsampled_vae.npz\")\n",
    "    second_vae_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_1_upsampled_vae.npz\")\n",
    "    \n",
    "    first_metadata_fileath = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_0__metadata.npz\")\n",
    "    second_metadata_fileath = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_1__metadata.npz\")\n",
    "    first_vae = np.load(first_vae_file)[\"vae_latents\"]\n",
    "    second_vae = np.load(second_vae_file)[\"vae_latents\"]\n",
    "    first_metadata = np.load(first_metadata_fileath, allow_pickle=True)\n",
    "    second_metadata = np.load(second_metadata_fileath, allow_pickle=True)\n",
    "    # convert first_metadata to a dict\n",
    "    first_metadata = dict(first_metadata)\n",
    "    second_metadata = dict(second_metadata)\n",
    "    print(\"negative audio (0)\")\n",
    "    for feature in features:\n",
    "        print(f\"{feature}: {first_metadata[feature]:0.2f}\")\n",
    "    first_audio = codec_decode(first_vae).play()\n",
    "    print()\n",
    "    print(\"positive audio (1)\")\n",
    "    for feature in features:\n",
    "        print(f\"{feature}: {second_metadata[feature]:0.2f}\")\n",
    "    second_audio = codec_decode(second_vae).play()\n",
    "    return first_audio, second_audio\n",
    "\n",
    "dirname = \"60789f90-e7e6-4a9c-806b-807bc8f9f876\"\n",
    "result = listen_to_vae(dirname)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d46534f9",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0bb92317",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5400ccd7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from joblib import Parallel, delayed\n",
    "\n",
    "BATCH_SIZE = 128\n",
    "\n",
    "def load_vaes_for_dir(dirname):\n",
    "    first_vae_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_0_upsampled_vae.npz\")\n",
    "    second_vae_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_1_upsampled_vae.npz\")\n",
    "    if os.path.exists(first_vae_file) and os.path.exists(second_vae_file):\n",
    "        first_vae = np.load(first_vae_file)[\"vae_latents\"]\n",
    "        second_vae = np.load(second_vae_file)[\"vae_latents\"]\n",
    "        return (dirname, first_vae, second_vae)\n",
    "    else:\n",
    "        return None\n",
    "\n",
    "# Parallel load all vae latents\n",
    "vae_data = Parallel(n_jobs=1)(\n",
    "    delayed(load_vaes_for_dir)(dirname) for dirname in tqdm(dirs)\n",
    ")\n",
    "# Filter out None\n",
    "vae_data = [item for item in vae_data if item is not None]\n",
    "\n",
    "results = {}\n",
    "\n",
    "# Process in batches\n",
    "for i in tqdm(range(0, len(vae_data), BATCH_SIZE)):\n",
    "    batch = vae_data[i:i+BATCH_SIZE]\n",
    "    dirnames = []\n",
    "    vae_latents = []\n",
    "    for dirname, first_vae, second_vae in batch:\n",
    "        dirnames.append(dirname)\n",
    "        vae_latents.append(torch.from_numpy(first_vae).unsqueeze(0).float())\n",
    "        vae_latents.append(torch.from_numpy(second_vae).unsqueeze(0).float())\n",
    "    # Stack and move to cuda\n",
    "    vae_latents_tensor = torch.cat(vae_latents, dim=0).cuda()\n",
    "    vae_latents_tensor *= CODEC_SCALE_FACTOR\n",
    "    with torch.no_grad():\n",
    "        scores = ear_model(vae_latents_tensor)\n",
    "    # scores shape: (2*batch_size,)\n",
    "    for j, dirname in enumerate(dirnames):\n",
    "        first_score = scores[2*j].item()\n",
    "        second_score = scores[2*j+1].item()\n",
    "        results[dirname] = {\n",
    "            \"0\": first_score,\n",
    "            \"1\": second_score\n",
    "        }\n",
    "\n",
    "# save the results\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e3b6c181",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pyloudnorm as pyln\n",
    "import soundfile as sf\n",
    "from joblib import Parallel, delayed\n",
    "\n",
    "def compute_loudness(dirname):\n",
    "    meter = pyln.Meter(48000)  # create a meter inside the function for thread/process safety\n",
    "    first_mp3_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_0.mp3\")\n",
    "    second_mp3_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_1.mp3\")\n",
    "    if os.path.exists(first_mp3_file) and os.path.exists(second_mp3_file):\n",
    "        try:\n",
    "            first_audio, _ = sf.read(first_mp3_file)\n",
    "            second_audio, _ = sf.read(second_mp3_file)\n",
    "            first_loudness = meter.integrated_loudness(first_audio)\n",
    "            second_loudness = meter.integrated_loudness(second_audio)\n",
    "            return (dirname, {\"0\": first_loudness, \"1\": second_loudness})\n",
    "        except Exception as e:\n",
    "            print(f\"Error processing {dirname}: {e}\")\n",
    "            return None\n",
    "    else:\n",
    "        return None\n",
    "\n",
    "results_list = Parallel(n_jobs=-1)(\n",
    "    delayed(compute_loudness)(dirname) for dirname in tqdm(dirs)\n",
    ")\n",
    "# Filter out None results and build the results dict\n",
    "# Filter out None results before unpacking\n",
    "filtered_results = [item for item in results_list if item is not None]\n",
    "results = {dirname: loudness for dirname, loudness in filtered_results}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "021381fb",
   "metadata": {},
   "outputs": [],
   "source": [
    "# save the results into dataframe\n",
    "import pandas as pd\n",
    "df = pd.DataFrame.from_dict(results, orient='index')\n",
    "\n",
    "# rename the index to be \"id\"\n",
    "df.reset_index(inplace=True)\n",
    "df.rename(columns={'index': 'id'}, inplace=True)\n",
    "\n",
    "df.to_csv(f\"{base_dir}/loudness_scores.csv\", index=False)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "05ee88fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "df.describe()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "68d63dd1",
   "metadata": {},
   "outputs": [],
   "source": [
    "for dirname in tqdm(dirs):\n",
    "    # find all mp3 files in the dir\n",
    "    first_mp3_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_0_upsampled_.npz\")\n",
    "    second_mp3_file = os.path.join(base_dir, dirname, f\"{dirname}_{model_name}_1_upsampled_vae.npz\")\n",
    "    if os.path.exists(first_mp3_file) and os.path.exists(second_mp3_file):\n",
    "        # now load both mp3 files and score them\n",
    "        first_mp3 = torch.from_numpy(np.load(first_mp3_file)[\"vae_latents\"]).unsqueeze(0).float().cuda()    \n",
    "        second_mp3 = torch.from_numpy(np.load(second_mp3_file)[\"vae_latents\"]).unsqueeze(0).float().cuda()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f430a0a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# now create a dataframe from the results\n",
    "import pandas as pd\n",
    "# Flatten the nested results dict into a flat list of dicts for DataFrame\n",
    "flat_results = []\n",
    "for clip_id, clip_data in results.items():\n",
    "    row = {'id': clip_id}\n",
    "    for version in ['0', '1']:\n",
    "        for stat in ['first_chunk', 'last_chunk', 'mean']:\n",
    "            row[f'{version}_{stat}'] = clip_data[version][stat]\n",
    "    flat_results.append(row)\n",
    "df = pd.DataFrame(flat_results)\n",
    "#loudness delta \n",
    "df['ear_v3_score_delta'] = df['1_mean'] - df['0_mean']\n",
    "df[\"0_first+last\"] = df[\"0_first_chunk\"] + df[\"0_last_chunk\"]\n",
    "df[\"1_first+last\"] = df[\"1_first_chunk\"] + df[\"1_last_chunk\"]\n",
    "\n",
    "# remove any rows where delta is Nan\n",
    "df = df[df['ear_v3_score_delta'].notna()]\n",
    "\n",
    "# sort by ear_v3_score_delta\n",
    "df = df.sort_values(by='1_first+last', ascending=False)\n",
    "\n",
    "# set rename the index to id \n",
    "# and then add a new index column\n",
    "df.reset_index(inplace=True)\n",
    "#df.rename(columns={'index': 'id'}, inplace=True)\n",
    "# save the dataframe to a csv file\n",
    "df.to_csv(f\"{base_dir}/ear_scores_s4862.csv\", index=False)\n",
    "print(len(df))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4945286a",
   "metadata": {},
   "outputs": [],
   "source": [
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c2df71ae",
   "metadata": {},
   "outputs": [],
   "source": [
    "df[\"id\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c79e8a76",
   "metadata": {},
   "outputs": [],
   "source": [
    "from suno_utils.audio import Audio\n",
    "\n",
    "row_idx = 4\n",
    "\n",
    "# get this row\n",
    "row = df.iloc[row_idx]\n",
    "print(row)\n",
    "\n",
    "# get the 0, and 1 mp3\n",
    "id = row['id']\n",
    "print(id)\n",
    "mp3_filepath_0 = os.path.join(base_dir, id, f\"{id}_{model_name}_0.mp3\")\n",
    "mp3_filepath_1 = os.path.join(base_dir, id, f\"{id}_{model_name}_1.mp3\")\n",
    "\n",
    "# load the mp3 files\n",
    "mp3_0 = Audio.from_file(mp3_filepath_0, n_channels=2)\n",
    "mp3_1 = Audio.from_file(mp3_filepath_1, n_channels=2)\n",
    "\n",
    "# get the scores\n",
    "score_0 = row['0_mean']\n",
    "score_1 = row['1_mean']\n",
    "\n",
    "print(score_0)\n",
    "mp3_0.play()\n",
    "\n",
    "print(score_1)\n",
    "mp3_1.play()\n",
    "\n",
    "\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5222336f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# now create a dataframe from the results\n",
    "import pandas as pd\n",
    "df = pd.DataFrame.from_dict(results, orient='index')\n",
    "#loudness delta \n",
    "df['loudness_delta'] = df['1'] - df['0']\n",
    "\n",
    "# remove any rows where delta is Nan\n",
    "df = df[df['loudness_delta'].notna()]\n",
    "\n",
    "# sort by loudness delta\n",
    "df = df.sort_values(by='loudness_delta', ascending=False)\n",
    "\n",
    "# set rename the index to id \n",
    "# and then add a new index column\n",
    "df.reset_index(inplace=True)\n",
    "df.rename(columns={'index': 'id'}, inplace=True)\n",
    "# save the dataframe to a csv file\n",
    "df.to_csv(f\"{base_dir}/loudness_scores.csv\", index=False)\n",
    "print(len(df))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "94410187",
   "metadata": {},
   "outputs": [],
   "source": [
    "df\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a8b6f912",
   "metadata": {},
   "outputs": [],
   "source": [
    "# i want to add a column with the index of the max value between 0 and 1\n",
    "df['max_index'] = df.idxmax(axis=1)\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f6ea2316",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "# concat the scores from 0 and 1 into a numpy array\n",
    "scores = np.concatenate([df['0'].values, df['1'].values], axis=0)\n",
    "# histogram of the scores \n",
    "plt.hist(scores, bins=100)\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7df7da78",
   "metadata": {},
   "outputs": [],
   "source": [
    "# histogram of the loudness delta\n",
    "plt.hist(df['loudness_delta'].values, bins=100)\n",
    "# plot the 5% and 95% quantiles\n",
    "plt.axvline(df['loudness_delta'].quantile(0.05), color='red', linestyle='--')\n",
    "plt.axvline(df['loudness_delta'].quantile(0.95), color='red', linestyle='--')\n",
    "print(df['loudness_delta'].quantile(0.05), df['loudness_delta'].quantile(0.95)  )\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "adc62bb0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# change the index column name to be \"clip_id\" \n",
    "# change the max_index column name to be \"chosen_slot\"\n",
    "df.rename(columns={'max_index': 'chosen_slot'}, inplace=True)\n",
    "df.head()\n",
    "\n",
    "# add an index column\n",
    "df.reset_index(inplace=True)\n",
    "df.rename(columns={'index': 'clip_id'}, inplace=True)\n",
    "\n",
    "# save the dataframe to a csv file\n",
    "df.to_csv(f\"{base_dir}/ear_scores.csv\", index=False)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "81e595af",
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "341adc4e",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "\n",
    "\n",
    "def best_worst_by_robust_combo(*features, weights=None, directions=None, return_scores=True):\n",
    "    \"\"\"\n",
    "    Robust-rank items given multiple feature lists.\n",
    "\n",
    "    Parameters\n",
    "    ----------\n",
    "    *features : array-like\n",
    "        One or more equal-length sequences (one value per item per feature).\n",
    "    weights : array-like or None\n",
    "        Optional weights per feature (same length as number of features). Defaults to equal weights.\n",
    "    directions : array-like of {+1, -1} or None\n",
    "        Optional sign per feature. +1 means higher is better for that feature,\n",
    "        -1 means lower is better. Defaults to +1 for all.\n",
    "    return_scores : bool\n",
    "        If True, also return the final combined scores array.\n",
    "\n",
    "    Returns\n",
    "    -------\n",
    "    best_idx : int\n",
    "    worst_idx : int\n",
    "    (scores) : np.ndarray, only if return_scores=True\n",
    "    \"\"\"\n",
    "    if len(features) == 0:\n",
    "        raise ValueError(\"Provide at least one feature.\")\n",
    "\n",
    "    X = [np.asarray(f, dtype=float) for f in features]\n",
    "    n = len(X[0])\n",
    "    if any(len(f) != n for f in X):\n",
    "        lens = [len(f) for f in X]\n",
    "        raise ValueError(f\"All features must have the same length. Got lengths: {lens}\")\n",
    "\n",
    "    m = len(X)  # number of features\n",
    "\n",
    "    # defaults\n",
    "    if weights is None:\n",
    "        weights = np.ones(m, dtype=float)\n",
    "    else:\n",
    "        weights = np.asarray(weights, dtype=float)\n",
    "        if len(weights) != m:\n",
    "            raise ValueError(\"weights must match number of features\")\n",
    "\n",
    "    if directions is None:\n",
    "        directions = np.ones(m, dtype=float)\n",
    "    else:\n",
    "        directions = np.asarray(directions, dtype=float)\n",
    "        if len(directions) != m:\n",
    "            raise ValueError(\"directions must match number of features\")\n",
    "        if not np.all(np.isin(directions, [+1, -1])):\n",
    "            raise ValueError(\"directions must be +1 or -1\")\n",
    "\n",
    "    # normalize weights to sum to 1\n",
    "    wsum = weights.sum()\n",
    "    if wsum <= 0:\n",
    "        raise ValueError(\"Sum of weights must be > 0\")\n",
    "    weights = weights / wsum\n",
    "\n",
    "    def robust_norm(x):\n",
    "        med = np.median(x)\n",
    "        q1, q3 = np.percentile(x, 25), np.percentile(x, 75)\n",
    "        iqr = max(q3 - q1, 1e-12)\n",
    "        return (x - med) / iqr\n",
    "\n",
    "    # robust-normalize each feature, apply direction (+1/-1), then weight and sum\n",
    "    scores = np.zeros(n, dtype=float)\n",
    "    for j, x in enumerate(X):\n",
    "        z = robust_norm(x) * directions[j]\n",
    "        scores += weights[j] * z\n",
    "\n",
    "    # best = largest score; worst = smallest score\n",
    "    best_idx = int(np.argmax(scores))\n",
    "    worst_idx = int(np.argmin(scores))\n",
    "\n",
    "    if return_scores:\n",
    "        return best_idx, worst_idx, scores\n",
    "    return best_idx, worst_idx"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "85592b22",
   "metadata": {},
   "outputs": [],
   "source": [
    "stereo_width_deltas = [-0.2, -0.1, 0.1, 0.34, 0.0]\n",
    "octave_deltas = [200, 100, 50, 25, 0]\n",
    "ear_v3_first_scores = [-3, 2, -4, 1, 10]\n",
    "directions = [-1, -1, 1]\n",
    "\n",
    "best_idx, worst_idx, scores = best_worst_by_robust_combo(stereo_width_deltas, octave_deltas, ear_v3_first_scores, directions=directions)\n",
    "\n",
    "print(best_idx, worst_idx)\n",
    "print(scores)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "780673aa",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "suno_diff",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
