{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Train a voice character on Colab\n",
    "\n",
    "Reusable for any character - mirrors `studio/voice.py`'s `train()` (same Applio\n",
    "commit, same 4 stages, same flags), so the model works with both Swara Studio\n",
    "(desktop) and the Swara Sound web app's Voices page.\n",
    "\n",
    "**Before you start:** Runtime -> Change runtime type -> **T4 GPU**.\n",
    "\n",
    "**1. Get your recordings into a zip.** Either:\n",
    "- **Swara Studio (desktop):** Voices -> fill in Name + Recordings -> **Prepare\n",
    "  for Colab...**. Writes `<name>_dataset.zip` next to the app.\n",
    "- **Swara Sound (web):** Voices -> name it, pick your recordings, **Zip &\n",
    "  upload dataset to Drive**. This uploads the zip straight to\n",
    "  `My Drive/SwaraStudioVoices/<name>_dataset.zip` for you - skip to step 4.\n",
    "\n",
    "**2. Set `MODEL_NAME`** in the next cell to that same name (letters/numbers,\n",
    "matches what you typed).\n",
    "\n",
    "**3. Give the notebook the zip - two ways, pick one:**\n",
    "- **Upload it directly** (simplest, nothing to set up). When the \"Get the\n",
    "  dataset\" cell runs, an upload button appears - pick the `<name>_dataset.zip`\n",
    "  from your computer. Fine for datasets under a few hundred MB.\n",
    "- **Or put it on Google Drive first**, at exactly:\n",
    "  `My Drive/SwaraStudioVoices/<name>_dataset.zip`\n",
    "  The notebook checks that exact path and uses it automatically, skipping the\n",
    "  upload prompt - handy if the notebook disconnects and you need to re-run.\n",
    "  (The web app's uploader already puts it exactly here.)\n",
    "\n",
    "**4. Run every cell top to bottom.** Keep the tab open (roughly 1-3 hours\n",
    "depending on epochs below - don't touch the tab while the training cell runs).\n",
    "\n",
    "**5. The last cell packages checkpoints at epochs 150 / 200 / 250** (so you can\n",
    "A/B them), copies them to `My Drive/SwaraStudioVoices/<name>_model/`, and also\n",
    "offers them as a direct browser download.\n",
    "\n",
    "**6. Bring the files back.** You need `<name>.index` and **one**\n",
    "`<name>_<N>e.pth` (start with `150e` - least risk of over-training artefacts on\n",
    "a small dataset). Either:\n",
    "- **Desktop:** put both in `C:\\SwaraStudio\\engines\\rvc\\applio\\logs\\<name>\\`\n",
    "  and rename the `.pth` to `<name>.pth`. Voices page then lists `<name>`.\n",
    "- **Web:** Voices -> **Import a trained voice** -> name it, pick the two\n",
    "  downloaded files (or grab them from Drive), **Import**.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- settings ---------------------------------------------------------------\n",
    "MODEL_NAME   = \"MyVoice\"      # <-- must match the Name you used in Swara Studio\n",
    "SAMPLE_RATE  = 40000           # Applio trains at 32000 / 40000 / 48000\n",
    "EPOCHS       = 250            # ~10-15 min of clean audio -> 150-250 is the useful band\n",
    "BATCH_SIZE   = 8              # T4 16 GB handles 8-12 at 40k\n",
    "SAVE_EVERY   = 25             # checkpoint every N epochs\n",
    "DRIVE_ROOT   = \"/content/drive/MyDrive/SwaraStudioVoices\"   # one folder for every character\n",
    "DATASET_ZIP  = f\"{MODEL_NAME}_dataset.zip\"                   # exactly what \"Prepare for Colab\" writes\n",
    "APPLIO_COMMIT = \"69b298e6336509b4caf01e3c2e43892b9075eb2a\"    # == engines/rvc/applio\n",
    "\n",
    "!nvidia-smi -L"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from google.colab import drive\n",
    "drive.mount('/content/drive')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- install Applio, pinned to the same commit the app uses ----------------\n",
    "%cd /content\n",
    "![ -d Applio ] || git clone https://github.com/IAHispano/Applio.git\n",
    "%cd /content/Applio\n",
    "!git checkout {APPLIO_COMMIT}\n",
    "!pip install -q -r requirements.txt\n",
    "!python core.py prerequisites\n",
    "# some clones lack this optional file; the model-extraction step hard-crashes\n",
    "# without it even though it only holds an author name\n",
    "import os, json\n",
    "os.makedirs('assets', exist_ok=True)\n",
    "if not os.path.exists('assets/config.json') or os.path.getsize('assets/config.json') == 0:\n",
    "    json.dump({'model_author': MODEL_NAME}, open('assets/config.json', 'w'))\n",
    "print('assets/config.json ok')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- get the dataset zip: Drive first, else ask you to upload it -----------\n",
    "import os, shutil\n",
    "from pathlib import Path\n",
    "\n",
    "os.makedirs(DRIVE_ROOT, exist_ok=True)\n",
    "drive_zip = Path(DRIVE_ROOT) / DATASET_ZIP\n",
    "local_zip = Path(\"/content\") / DATASET_ZIP\n",
    "\n",
    "if drive_zip.exists():\n",
    "    print(f\"Found it on Drive: {drive_zip}\")\n",
    "    shutil.copy(drive_zip, local_zip)\n",
    "else:\n",
    "    print(f\"Not on Drive at {drive_zip}.\")\n",
    "    print(\"Upload the dataset zip now (Swara Studio -> Voices -> Prepare for Colab):\")\n",
    "    from google.colab import files\n",
    "    uploaded = files.upload()\n",
    "    assert uploaded, \"no file uploaded - re-run this cell and pick the zip\"\n",
    "    fn = next(iter(uploaded))\n",
    "    shutil.move(fn, local_zip)\n",
    "\n",
    "print(f\"Using dataset: {local_zip}  ({local_zip.stat().st_size / 1e6:.1f} MB)\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- unzip into Applio's dataset folder -------------------------------------\n",
    "import zipfile, glob\n",
    "dst = f\"/content/Applio/dataset/{MODEL_NAME}\"\n",
    "os.makedirs(dst, exist_ok=True)\n",
    "with zipfile.ZipFile(local_zip) as z:\n",
    "    z.extractall(dst)\n",
    "exts = (\".wav\", \".mp3\", \".flac\", \".ogg\", \".m4a\")\n",
    "n = sum(1 for f in glob.glob(f\"{dst}/**/*\", recursive=True) if f.lower().endswith(exts))\n",
    "assert n > 0, f\"No audio found after extracting {local_zip} - is it the right zip?\"\n",
    "print(f\"{n} audio file(s) ready in {dst}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- Stage 1/4: preprocess ----------------------------------------------\n",
    "%cd /content/Applio\n",
    "!python core.py preprocess --model-name {MODEL_NAME} \\\n",
    "  --dataset-path /content/Applio/dataset/{MODEL_NAME} \\\n",
    "  --sample-rate {SAMPLE_RATE} --cut-preprocess Automatic \\\n",
    "  --noise-reduction --normalization-mode post"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- Stage 2/4: extract features (pitch + speaker embedding) ------------\n",
    "!python core.py extract --model-name {MODEL_NAME} --sample-rate {SAMPLE_RATE} \\\n",
    "  --f0-method rmvpe --gpu 0 --embedder-model contentvec"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- Stage 3/4: train (the long one - keep the tab open, don't touch it) --\n",
    "!python core.py train --model-name {MODEL_NAME} --total-epoch {EPOCHS} \\\n",
    "  --sample-rate {SAMPLE_RATE} --batch-size {BATCH_SIZE} --gpu 0 \\\n",
    "  --save-every-epoch {SAVE_EVERY} --save-every-weights \\\n",
    "  --pretrained --vocoder HiFi-GAN"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- Stage 4/4: build the .index retrieval file ----------------------\n",
    "!python core.py index --model-name {MODEL_NAME} --index-algorithm Auto"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- package usable models, copy to Drive, offer a direct download ---------\n",
    "# Applio's own end-of-training extraction is flaky, so do it here from the\n",
    "# G_ checkpoints. Packages epochs 150 / 200 / 250 so you can A/B.\n",
    "%cd /content/Applio\n",
    "import os, json, glob, shutil, torch\n",
    "os.makedirs('assets', exist_ok=True)\n",
    "if not os.path.exists('assets/config.json') or os.path.getsize('assets/config.json') == 0:\n",
    "    json.dump({'model_author': MODEL_NAME}, open('assets/config.json', 'w'))\n",
    "from rvc.train.utils import HParams\n",
    "from rvc.train.process.extract_model import extract_model\n",
    "\n",
    "EXP = f\"logs/{MODEL_NAME}\"\n",
    "cfg = HParams(**json.load(open(f\"{EXP}/config.json\")))\n",
    "\n",
    "def step_for(epoch):\n",
    "    gs = sorted(int(os.path.basename(p)[2:-4]) for p in glob.glob(f\"{EXP}/G_*.pth\"))\n",
    "    per = gs[0] // SAVE_EVERY if gs else 31          # steps per epoch\n",
    "    want = epoch * per\n",
    "    return min(gs, key=lambda s: abs(s - want)) if gs else None\n",
    "\n",
    "made = []\n",
    "for epoch in (150, 200, 250):\n",
    "    step = step_for(epoch)\n",
    "    g = f\"{EXP}/G_{step}.pth\" if step else None\n",
    "    if not g or not os.path.exists(g):\n",
    "        print(\"skip (no checkpoint near epoch)\", epoch); continue\n",
    "    ck = torch.load(g, map_location=\"cpu\", weights_only=False)\n",
    "    sd = ck[\"model\"] if \"model\" in ck else ck\n",
    "    out = f\"{EXP}/{MODEL_NAME}_{epoch}e.pth\"\n",
    "    extract_model(ckpt=sd, sr=cfg.data.sample_rate, name=MODEL_NAME,\n",
    "                  model_path=out, epoch=epoch, step=step, hps=cfg, vocoder=\"HiFi-GAN\")\n",
    "    if os.path.exists(out):\n",
    "        made.append(out); print(\"  OK\", out)\n",
    "\n",
    "idx = glob.glob(f\"{EXP}/*.index\")\n",
    "assert made and idx, f\"nothing to package: pth={made} idx={idx}\"\n",
    "\n",
    "drive_dst = f\"{DRIVE_ROOT}/{MODEL_NAME}_model\"\n",
    "os.makedirs(drive_dst, exist_ok=True)\n",
    "for p in made + idx:\n",
    "    shutil.copy(p, drive_dst); print(\"  copied to Drive:\", os.path.basename(p))\n",
    "\n",
    "print(f\"\\nSaved to Drive: {drive_dst}/\")\n",
    "print(f\"Also downloading {os.path.basename(made[0])} + {os.path.basename(idx[0])} to your browser...\")\n",
    "from google.colab import files\n",
    "files.download(made[0])\n",
    "files.download(idx[0])\n",
    "\n",
    "print(f\"\\nOn the laptop: put {os.path.basename(idx[0])} and ONE *_<N>e.pth (start with 150e) in\")\n",
    "print(f\"C:\\\\SwaraStudio\\\\engines\\\\rvc\\\\applio\\\\logs\\\\{MODEL_NAME}\\\\ and rename the pth to {MODEL_NAME}.pth\")"
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "provenance": [],
   "gpuType": "T4"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}