Spaces:
Sleeping
Sleeping
Broulaye Doumbia commited on
Commit ·
76aac1b
1
Parent(s): cc8b90c
restructuring
Browse files- .gitattributes +3 -0
- .gitignore +8 -0
- docs/kaggle_mcp_setup.md +181 -0
- notebooks/bootstrap_repos.ipynb +60 -31
- notebooks/kaggle_master_trainer.ipynb +0 -253
- notebooks/kaggle_master_trainer/kaggle_master_trainer.ipynb +1352 -0
- notebooks/{kernel-metadata.json → kaggle_master_trainer/kernel-metadata.json} +0 -0
- notebooks/train_colab.ipynb +55 -30
- notebooks/train_fula_tts/kernel-metadata.json +14 -0
- notebooks/{train_fula_tts.ipynb → train_fula_tts/train_fula_tts.ipynb} +0 -0
.gitattributes
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.ipynb filter=nbstripout
|
| 2 |
+
*.zpln filter=nbstripout
|
| 3 |
+
*.ipynb diff=ipynb
|
.gitignore
CHANGED
|
@@ -64,3 +64,11 @@ models/
|
|
| 64 |
.pytest_cache/
|
| 65 |
htmlcov/
|
| 66 |
.coverage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
.pytest_cache/
|
| 65 |
htmlcov/
|
| 66 |
.coverage
|
| 67 |
+
|
| 68 |
+
# Kaggle credentials (NEVER commit these)
|
| 69 |
+
kaggle.json
|
| 70 |
+
.kaggle/
|
| 71 |
+
|
| 72 |
+
# Kaggle kernel outputs (pulled via `kaggle kernels output`)
|
| 73 |
+
runs/
|
| 74 |
+
notebooks/**/runs/
|
docs/kaggle_mcp_setup.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Kaggle MCP Setup — Connecting Claude Desktop to Your Kaggle Account
|
| 2 |
+
|
| 3 |
+
*Audience: anyone on Sahel-Voice-Lab who wants a Kaggle-aware Claude on their own machine*
|
| 4 |
+
*Last updated: 2026-04-20*
|
| 5 |
+
|
| 6 |
+
## What this is, and what it isn't
|
| 7 |
+
|
| 8 |
+
The Kaggle MCP (Model Context Protocol) is a small server that wraps the Kaggle API and exposes it as tools a Claude client can call. Once set up, you can ask Claude things like "what's the status of my training kernel" or "find Bambara datasets on Kaggle" and it will actually reach Kaggle and answer with live data, rather than guessing.
|
| 9 |
+
|
| 10 |
+
**Important context:** the MCP runs on your **local machine** and connects to a **local Claude client** — specifically Claude Desktop. It does **not** plug into this Cowork session. Cowork only uses MCPs that are in its registry, and Kaggle isn't there yet. So this setup gives you a parallel, Kaggle-aware Claude in your desktop app; it doesn't change what's available in Cowork.
|
| 11 |
+
|
| 12 |
+
If you haven't installed Claude Desktop yet, that's step zero — grab it from [claude.ai/download](https://claude.ai/download) and sign in with the same account you already use.
|
| 13 |
+
|
| 14 |
+
## Which implementation to install
|
| 15 |
+
|
| 16 |
+
Multiple implementations exist. Kaggle has their own page at [kaggle.com/docs/mcp](https://www.kaggle.com/docs/mcp), plus several community ports on GitHub:
|
| 17 |
+
|
| 18 |
+
- `54yyyu/kaggle-mcp` — covers dataset, competition, **and kernel** operations. Recommended for Sahel-Voice-Lab because we care about kernel operations (pushing, monitoring, pulling results), not just dataset search.
|
| 19 |
+
- `Dishant27/kaggle-MCP` — competition-focused. Less relevant for us.
|
| 20 |
+
- `KrishnaPramodParupudi/kaggle-mcp-server` — also Claude Desktop-oriented; feature-wise similar to `54yyyu/kaggle-mcp`.
|
| 21 |
+
|
| 22 |
+
Start with `54yyyu/kaggle-mcp`. Cross-check against the official Kaggle docs page for whatever they're currently recommending — the MCP landscape shifts every few months.
|
| 23 |
+
|
| 24 |
+
## Prerequisites
|
| 25 |
+
|
| 26 |
+
- Python 3.10 or newer
|
| 27 |
+
- `git` installed
|
| 28 |
+
- A Kaggle account
|
| 29 |
+
- Your Kaggle API credentials already set up at `~/.kaggle/kaggle.json` with mode 600 (if not, see the notebook collaboration guide or the short version below)
|
| 30 |
+
- Claude Desktop installed and opened at least once
|
| 31 |
+
|
| 32 |
+
Quick credentials sanity check:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
ls -l ~/.kaggle/kaggle.json
|
| 36 |
+
# expect: -rw------- ... kaggle.json
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
If missing: Kaggle → Settings → API → **Create New API Token**, move `kaggle.json` to `~/.kaggle/kaggle.json`, `chmod 600 ~/.kaggle/kaggle.json`.
|
| 40 |
+
|
| 41 |
+
## Step-by-step setup
|
| 42 |
+
|
| 43 |
+
### Step 1 — Install the MCP server
|
| 44 |
+
|
| 45 |
+
Clone into a persistent folder (not `/tmp`):
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
mkdir -p ~/tools
|
| 49 |
+
cd ~/tools
|
| 50 |
+
git clone https://github.com/54yyyu/kaggle-mcp.git
|
| 51 |
+
cd kaggle-mcp
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Check the repo's README for the exact install command. It will be one of:
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
pip install -e .
|
| 58 |
+
# or
|
| 59 |
+
uv pip install -e .
|
| 60 |
+
# or
|
| 61 |
+
pip install -r requirements.txt
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
Strongly consider using a dedicated virtualenv or conda env so the MCP's dependencies don't collide with Sahel-Voice-Lab's. Example:
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
python -m venv ~/tools/kaggle-mcp/.venv
|
| 68 |
+
source ~/tools/kaggle-mcp/.venv/bin/activate
|
| 69 |
+
pip install -e .
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Confirm the server launches:
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
python -m kaggle_mcp --help
|
| 76 |
+
# or whatever entry point the README names
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
If that errors, stop and fix — don't move on until this command responds cleanly.
|
| 80 |
+
|
| 81 |
+
**Take note of the absolute Python path** you used, because Claude Desktop will need it:
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
which python
|
| 85 |
+
# e.g. /Users/broulaye/tools/kaggle-mcp/.venv/bin/python
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Step 2 — Locate Claude Desktop's config file
|
| 89 |
+
|
| 90 |
+
Platform-specific:
|
| 91 |
+
|
| 92 |
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 93 |
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
| 94 |
+
- **Linux:** `~/.config/Claude/claude_desktop_config.json`
|
| 95 |
+
|
| 96 |
+
If the file doesn't exist, create it with an empty object `{}`.
|
| 97 |
+
|
| 98 |
+
### Step 3 — Add the Kaggle MCP to the config
|
| 99 |
+
|
| 100 |
+
Open the file and add an `mcpServers` section. Example (substitute the absolute Python path from Step 1, and confirm the `args` against the repo's README — that's the most commonly-differing piece):
|
| 101 |
+
|
| 102 |
+
```json
|
| 103 |
+
{
|
| 104 |
+
"mcpServers": {
|
| 105 |
+
"kaggle": {
|
| 106 |
+
"command": "/Users/broulaye/tools/kaggle-mcp/.venv/bin/python",
|
| 107 |
+
"args": ["-m", "kaggle_mcp"],
|
| 108 |
+
"env": {
|
| 109 |
+
"KAGGLE_USERNAME": "your_kaggle_username",
|
| 110 |
+
"KAGGLE_KEY": "your_kaggle_api_key"
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Two frequent gotchas:
|
| 118 |
+
|
| 119 |
+
1. **`command` must be an absolute path** to the Python that has `kaggle_mcp` installed. If you used a virtualenv or conda env, don't rely on `"python"` alone — Claude Desktop launches the system Python, which won't find the package.
|
| 120 |
+
2. **`KAGGLE_USERNAME` / `KAGGLE_KEY`** come from your `~/.kaggle/kaggle.json`. Some implementations read that file automatically and you can omit the `env` block; the README will say.
|
| 121 |
+
|
| 122 |
+
Save the file.
|
| 123 |
+
|
| 124 |
+
### Step 4 — Fully restart Claude Desktop
|
| 125 |
+
|
| 126 |
+
Not just close the window — **quit completely** (⌘Q on macOS, right-click tray icon → Quit on Windows). Reopen. MCP servers start with the client.
|
| 127 |
+
|
| 128 |
+
### Step 5 — Verify it loaded
|
| 129 |
+
|
| 130 |
+
In a new Claude Desktop conversation, try:
|
| 131 |
+
|
| 132 |
+
```
|
| 133 |
+
Using the Kaggle MCP, list my recent kernels.
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
If it works, Claude will call the MCP and return your kernels (including `sahel-kaggle-master-trainer`).
|
| 137 |
+
|
| 138 |
+
If it doesn't, open **Settings → Developer → Open Logs** and look for lines mentioning `kaggle` or `mcp`. Common failures:
|
| 139 |
+
|
| 140 |
+
- `ModuleNotFoundError: kaggle_mcp` — `command` points to the wrong Python. Paste the absolute path from `which python` inside your virtualenv.
|
| 141 |
+
- `401 Unauthorized` — credentials missing or wrong. Test first with `kaggle kernels list --mine` in a terminal; if that fails, the MCP will fail the same way.
|
| 142 |
+
- `spawn ENOENT` — `command` needs to be a full path, not just `"python"`.
|
| 143 |
+
- MCP starts but tools are missing — check the repo's README; the tool list can evolve.
|
| 144 |
+
|
| 145 |
+
## First useful things to ask it, for Sahel-Voice-Lab
|
| 146 |
+
|
| 147 |
+
Once it's live, high-leverage uses for our project:
|
| 148 |
+
|
| 149 |
+
- **Dataset discovery.** "Find Bambara or Fula ASR datasets on Kaggle I haven't used yet." Feeds Stage C eval-set work.
|
| 150 |
+
- **Kernel monitoring.** "What's the status of `ous-sow/sahel-kaggle-master-trainer`? Show the last run's log tail." Replaces tab-switching.
|
| 151 |
+
- **Dataset download into the repo.** "Download RobotsMali Jeli-ASR to `~/projects/sahel-agri-voice/data/raw/`."
|
| 152 |
+
- **Prior-art scans.** "Show me public Kaggle notebooks using `openai/whisper-large-v3-turbo` for African languages."
|
| 153 |
+
|
| 154 |
+
## Use with care
|
| 155 |
+
|
| 156 |
+
- **Pushing training kernels.** Technically possible, but `kaggle kernels push` replaces the shared kernel on Kaggle's side. You want human review and coordination with your collaborator before that happens. Use the CLI workflow from `docs/notebook_collaboration.md` for pushes; use the MCP for reads and discovery.
|
| 157 |
+
- **Anything that costs GPU hours.** The 30-hour weekly quota still applies; the MCP doesn't know your budget.
|
| 158 |
+
- **Credentials in the config file.** `claude_desktop_config.json` sits in your home directory in plaintext. If your machine is shared, treat the `env` block as a secret; prefer the MCP implementation that reads `~/.kaggle/kaggle.json` directly (so nothing sensitive lands in the config file).
|
| 159 |
+
|
| 160 |
+
## What the MCP won't do
|
| 161 |
+
|
| 162 |
+
- It won't debug a T4 training crash interactively. You can push and watch, but the crash itself still lives in Kaggle's logs, eyeballed by a human.
|
| 163 |
+
- It won't raise your GPU quota.
|
| 164 |
+
- It won't help with merge conflicts or Kaggle's rate limits.
|
| 165 |
+
- It won't work in this Cowork session — only Claude Desktop (or Claude Code, if you've set up MCP there separately).
|
| 166 |
+
- It doesn't replace the `kaggle` CLI; it's a friendlier, conversational interface to the same API.
|
| 167 |
+
|
| 168 |
+
## Troubleshooting summary
|
| 169 |
+
|
| 170 |
+
| Symptom | Likely cause | Fix |
|
| 171 |
+
|---|---|---|
|
| 172 |
+
| Claude says "I don't have a Kaggle tool" | MCP didn't start | Check Developer Logs; fix `command` path |
|
| 173 |
+
| `ModuleNotFoundError: kaggle_mcp` | Wrong Python interpreter | Use absolute path to virtualenv Python |
|
| 174 |
+
| `401 Unauthorized` | Bad/missing credentials | Re-test with `kaggle kernels list --mine` in terminal |
|
| 175 |
+
| `spawn ENOENT` | Relative command path | Absolute path in `command` |
|
| 176 |
+
| Tool names don't match the docs | Implementation changed | Re-read the repo's README; align your prompts |
|
| 177 |
+
| Works once, then stops | Claude Desktop not fully quit on last restart | Fully quit, reopen |
|
| 178 |
+
|
| 179 |
+
## TL;DR
|
| 180 |
+
|
| 181 |
+
Install `54yyyu/kaggle-mcp` locally into a virtualenv, make sure `~/.kaggle/kaggle.json` is present, point Claude Desktop's `claude_desktop_config.json` at the virtualenv's Python, fully quit and reopen. Use for reads and discovery; keep the CLI for kernel pushes. Won't work in Cowork — Claude Desktop only.
|
notebooks/bootstrap_repos.ipynb
CHANGED
|
@@ -1,26 +1,8 @@
|
|
| 1 |
{
|
| 2 |
-
"nbformat": 4,
|
| 3 |
-
"nbformat_minor": 5,
|
| 4 |
-
"metadata": {
|
| 5 |
-
"kernelspec": {
|
| 6 |
-
"display_name": "Python 3",
|
| 7 |
-
"language": "python",
|
| 8 |
-
"name": "python3"
|
| 9 |
-
},
|
| 10 |
-
"language_info": {
|
| 11 |
-
"name": "python",
|
| 12 |
-
"version": "3.10.0"
|
| 13 |
-
},
|
| 14 |
-
"colab": {
|
| 15 |
-
"provenance": [],
|
| 16 |
-
"gpuType": "T4"
|
| 17 |
-
},
|
| 18 |
-
"accelerator": "GPU"
|
| 19 |
-
},
|
| 20 |
"cells": [
|
| 21 |
{
|
| 22 |
"cell_type": "markdown",
|
| 23 |
-
"id": "
|
| 24 |
"metadata": {},
|
| 25 |
"source": [
|
| 26 |
"# 🌾 Sahel-Agri Voice AI — One-Time Bootstrap\n",
|
|
@@ -44,7 +26,7 @@
|
|
| 44 |
{
|
| 45 |
"cell_type": "code",
|
| 46 |
"execution_count": null,
|
| 47 |
-
"id": "
|
| 48 |
"metadata": {},
|
| 49 |
"outputs": [],
|
| 50 |
"source": [
|
|
@@ -60,7 +42,7 @@
|
|
| 60 |
{
|
| 61 |
"cell_type": "code",
|
| 62 |
"execution_count": null,
|
| 63 |
-
"id": "
|
| 64 |
"metadata": {},
|
| 65 |
"outputs": [],
|
| 66 |
"source": [
|
|
@@ -78,15 +60,44 @@
|
|
| 78 |
{
|
| 79 |
"cell_type": "code",
|
| 80 |
"execution_count": null,
|
| 81 |
-
"id": "
|
| 82 |
"metadata": {},
|
| 83 |
"outputs": [],
|
| 84 |
-
"source":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
},
|
| 86 |
{
|
| 87 |
"cell_type": "code",
|
| 88 |
"execution_count": null,
|
| 89 |
-
"id": "
|
| 90 |
"metadata": {},
|
| 91 |
"outputs": [],
|
| 92 |
"source": [
|
|
@@ -129,7 +140,7 @@
|
|
| 129 |
{
|
| 130 |
"cell_type": "code",
|
| 131 |
"execution_count": null,
|
| 132 |
-
"id": "
|
| 133 |
"metadata": {},
|
| 134 |
"outputs": [],
|
| 135 |
"source": [
|
|
@@ -163,7 +174,7 @@
|
|
| 163 |
{
|
| 164 |
"cell_type": "code",
|
| 165 |
"execution_count": null,
|
| 166 |
-
"id": "
|
| 167 |
"metadata": {},
|
| 168 |
"outputs": [],
|
| 169 |
"source": [
|
|
@@ -216,7 +227,7 @@
|
|
| 216 |
{
|
| 217 |
"cell_type": "code",
|
| 218 |
"execution_count": null,
|
| 219 |
-
"id": "
|
| 220 |
"metadata": {},
|
| 221 |
"outputs": [],
|
| 222 |
"source": [
|
|
@@ -245,7 +256,7 @@
|
|
| 245 |
{
|
| 246 |
"cell_type": "code",
|
| 247 |
"execution_count": null,
|
| 248 |
-
"id": "
|
| 249 |
"metadata": {},
|
| 250 |
"outputs": [],
|
| 251 |
"source": [
|
|
@@ -280,7 +291,7 @@
|
|
| 280 |
{
|
| 281 |
"cell_type": "code",
|
| 282 |
"execution_count": null,
|
| 283 |
-
"id": "
|
| 284 |
"metadata": {},
|
| 285 |
"outputs": [],
|
| 286 |
"source": [
|
|
@@ -304,5 +315,23 @@
|
|
| 304 |
" print('\\n⚠️ Some adapters are missing — check the training cells above for errors.')"
|
| 305 |
]
|
| 306 |
}
|
| 307 |
-
]
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
"cells": [
|
| 3 |
{
|
| 4 |
"cell_type": "markdown",
|
| 5 |
+
"id": "0",
|
| 6 |
"metadata": {},
|
| 7 |
"source": [
|
| 8 |
"# 🌾 Sahel-Agri Voice AI — One-Time Bootstrap\n",
|
|
|
|
| 26 |
{
|
| 27 |
"cell_type": "code",
|
| 28 |
"execution_count": null,
|
| 29 |
+
"id": "1",
|
| 30 |
"metadata": {},
|
| 31 |
"outputs": [],
|
| 32 |
"source": [
|
|
|
|
| 42 |
{
|
| 43 |
"cell_type": "code",
|
| 44 |
"execution_count": null,
|
| 45 |
+
"id": "2",
|
| 46 |
"metadata": {},
|
| 47 |
"outputs": [],
|
| 48 |
"source": [
|
|
|
|
| 60 |
{
|
| 61 |
"cell_type": "code",
|
| 62 |
"execution_count": null,
|
| 63 |
+
"id": "3",
|
| 64 |
"metadata": {},
|
| 65 |
"outputs": [],
|
| 66 |
+
"source": [
|
| 67 |
+
"# Cell 3 — HuggingFace login\n",
|
| 68 |
+
"# Colab: 🔑 icon (left sidebar) → Add new secret → name=HF_TOKEN\n",
|
| 69 |
+
"import os\n",
|
| 70 |
+
"try:\n",
|
| 71 |
+
" from google.colab import userdata # type: ignore\n",
|
| 72 |
+
" HF_TOKEN = userdata.get('HF_TOKEN')\n",
|
| 73 |
+
"except Exception:\n",
|
| 74 |
+
" HF_TOKEN = os.environ.get('HF_TOKEN', '')\n",
|
| 75 |
+
"\n",
|
| 76 |
+
"if not HF_TOKEN:\n",
|
| 77 |
+
" raise ValueError(\n",
|
| 78 |
+
" 'HF_TOKEN not found.\\n'\n",
|
| 79 |
+
" 'Colab: click the 🔑 icon → Add new secret → name=HF_TOKEN'\n",
|
| 80 |
+
" )\n",
|
| 81 |
+
"\n",
|
| 82 |
+
"from huggingface_hub import login, HfApi\n",
|
| 83 |
+
"login(token=HF_TOKEN, add_to_git_credential=False)\n",
|
| 84 |
+
"api = HfApi(token=HF_TOKEN)\n",
|
| 85 |
+
"\n",
|
| 86 |
+
"HF_USERNAME = 'ous-sow'\n",
|
| 87 |
+
"FEEDBACK_REPO_ID = f'{HF_USERNAME}/sahel-agri-feedback'\n",
|
| 88 |
+
"ADAPTER_REPO_ID = f'{HF_USERNAME}/sahel-agri-adapters'\n",
|
| 89 |
+
"SPACE_REPO_ID = f'{HF_USERNAME}/sahel-agri-voice'\n",
|
| 90 |
+
"# whisper-small trains on Colab T4 in ~25 min and runs on CPU in ~10s.\n",
|
| 91 |
+
"# Change to 'openai/whisper-large-v3-turbo' only if you upgrade to a GPU Space.\n",
|
| 92 |
+
"WHISPER_MODEL_ID = 'openai/whisper-small'\n",
|
| 93 |
+
"\n",
|
| 94 |
+
"print(f'✅ Logged in as {HF_USERNAME}')"
|
| 95 |
+
]
|
| 96 |
},
|
| 97 |
{
|
| 98 |
"cell_type": "code",
|
| 99 |
"execution_count": null,
|
| 100 |
+
"id": "4",
|
| 101 |
"metadata": {},
|
| 102 |
"outputs": [],
|
| 103 |
"source": [
|
|
|
|
| 140 |
{
|
| 141 |
"cell_type": "code",
|
| 142 |
"execution_count": null,
|
| 143 |
+
"id": "5",
|
| 144 |
"metadata": {},
|
| 145 |
"outputs": [],
|
| 146 |
"source": [
|
|
|
|
| 174 |
{
|
| 175 |
"cell_type": "code",
|
| 176 |
"execution_count": null,
|
| 177 |
+
"id": "6",
|
| 178 |
"metadata": {},
|
| 179 |
"outputs": [],
|
| 180 |
"source": [
|
|
|
|
| 227 |
{
|
| 228 |
"cell_type": "code",
|
| 229 |
"execution_count": null,
|
| 230 |
+
"id": "7",
|
| 231 |
"metadata": {},
|
| 232 |
"outputs": [],
|
| 233 |
"source": [
|
|
|
|
| 256 |
{
|
| 257 |
"cell_type": "code",
|
| 258 |
"execution_count": null,
|
| 259 |
+
"id": "8",
|
| 260 |
"metadata": {},
|
| 261 |
"outputs": [],
|
| 262 |
"source": [
|
|
|
|
| 291 |
{
|
| 292 |
"cell_type": "code",
|
| 293 |
"execution_count": null,
|
| 294 |
+
"id": "9",
|
| 295 |
"metadata": {},
|
| 296 |
"outputs": [],
|
| 297 |
"source": [
|
|
|
|
| 315 |
" print('\\n⚠️ Some adapters are missing — check the training cells above for errors.')"
|
| 316 |
]
|
| 317 |
}
|
| 318 |
+
],
|
| 319 |
+
"metadata": {
|
| 320 |
+
"accelerator": "GPU",
|
| 321 |
+
"colab": {
|
| 322 |
+
"gpuType": "T4",
|
| 323 |
+
"provenance": []
|
| 324 |
+
},
|
| 325 |
+
"kernelspec": {
|
| 326 |
+
"display_name": "Python 3",
|
| 327 |
+
"language": "python",
|
| 328 |
+
"name": "python3"
|
| 329 |
+
},
|
| 330 |
+
"language_info": {
|
| 331 |
+
"name": "python",
|
| 332 |
+
"version": "3.10.0"
|
| 333 |
+
}
|
| 334 |
+
},
|
| 335 |
+
"nbformat": 4,
|
| 336 |
+
"nbformat_minor": 5
|
| 337 |
+
}
|
notebooks/kaggle_master_trainer.ipynb
DELETED
|
@@ -1,253 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"nbformat": 4,
|
| 3 |
-
"nbformat_minor": 5,
|
| 4 |
-
"metadata": {
|
| 5 |
-
"kernelspec": {
|
| 6 |
-
"display_name": "Python 3",
|
| 7 |
-
"language": "python",
|
| 8 |
-
"name": "python3"
|
| 9 |
-
},
|
| 10 |
-
"language_info": {
|
| 11 |
-
"name": "python",
|
| 12 |
-
"version": "3.10.12"
|
| 13 |
-
}
|
| 14 |
-
},
|
| 15 |
-
"cells": [
|
| 16 |
-
{
|
| 17 |
-
"cell_type": "markdown",
|
| 18 |
-
"id": "title",
|
| 19 |
-
"metadata": {},
|
| 20 |
-
"source": "# 🌾 Sahel-Voice-Core — Kaggle Master Trainer\n\n**Deep Sleep Fine-Tuning** for `openai/whisper-small` using LoRA (PEFT).\n\nThis single notebook replaces `bootstrap_repos.ipynb`, `train_colab.ipynb`, and `train_fula_tts.ipynb`.\n\n### Data sources integrated\n| Source | Contents | Used for |\n|--------|----------|----------|\n| `ous-sow/sahel-agri-feedback` | `vocabulary.jsonl` + `corrections.jsonl` + audio | Primary fine-tuning signal |\n| `google/WaxalNLP` (bam + ful subsets) | Native speaker recordings | Baseline Bambara & Fula |\n| External datasets (configurable) | e.g. `mozilla-foundation/common_voice_13_0` | Coverage expansion |\n\n### Before running\n1. **Runtime → Accelerator → GPU T4 × 1** \n2. Add-ons → Secrets → `HF_TOKEN` (toggle Attach ON)\n3. Edit **Cell 3** to set your language and version tag prefix"
|
| 21 |
-
},
|
| 22 |
-
{
|
| 23 |
-
"cell_type": "code",
|
| 24 |
-
"execution_count": null,
|
| 25 |
-
"id": "cell-gpu",
|
| 26 |
-
"metadata": {},
|
| 27 |
-
"outputs": [],
|
| 28 |
-
"source": "# ── Cell 1: GPU check ────────────────────────────────────────────────────────\nimport subprocess, sys\n\nresult = subprocess.run(['nvidia-smi'], capture_output=True, text=True)\nif result.returncode != 0:\n raise RuntimeError('No GPU detected. Runtime → Accelerator → GPU T4 × 1')\nprint(result.stdout[:600])\n\nimport torch\nprint(f'PyTorch : {torch.__version__}')\nprint(f'CUDA avail: {torch.cuda.is_available()}')\nif torch.cuda.is_available():\n print(f'GPU : {torch.cuda.get_device_name(0)}')\n cap = torch.cuda.get_device_capability(0)\n print(f'Compute : {cap[0]}.{cap[1]}')\n if cap[0] < 7:\n print('⚠️ Compute < 7.0 — bitsandbytes 8-bit ops may not work. Switch to T4 (CC 7.5).')\nprint('✅ GPU ready')"
|
| 29 |
-
},
|
| 30 |
-
{
|
| 31 |
-
"cell_type": "code",
|
| 32 |
-
"execution_count": null,
|
| 33 |
-
"id": "cell-install",
|
| 34 |
-
"metadata": {},
|
| 35 |
-
"outputs": [],
|
| 36 |
-
"source": [
|
| 37 |
-
"# -- Cell 2: Install minimal missing dependencies ----------------------------\n# We do NOT use PEFT/LoRA, so system transformers/numpy/scipy are fine as-is.\n# Kaggle does not ship jiwer (WER metric) -- install it now.\nimport subprocess, sys\n\nsubprocess.check_call([\n sys.executable, '-m', 'pip', 'install', '-q', 'jiwer==3.0.4',\n])\n\n# datasets >= 4.0 uses torchcodec for audio decoding. Install if missing.\ntry:\n import torchcodec # noqa\nexcept ImportError:\n print('torchcodec not found — installing to match torch ...')\n try:\n subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', 'torchcodec'])\n except subprocess.CalledProcessError:\n import torch as _t\n _tv = _t.__version__.split('+')[0]\n _pin = {'2.4': '0.1.*', '2.5': '0.2.*', '2.6': '0.3.*', '2.7': '0.4.*', '2.8': '0.4.*'}.get(_tv[:3])\n if _pin:\n subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', f'torchcodec=={_pin}'])\n else:\n print(f'⚠️ Unknown torch {_tv}; install torchcodec manually if audio decoding fails')\n\nimport torch\nprint(f\"torch : {torch.__version__}\")\nprint(f\"CUDA avail : {torch.cuda.is_available()}\")\nif torch.cuda.is_available():\n print(f\"GPU : {torch.cuda.get_device_name(0)}\")\n\nimport transformers, datasets as ds_lib\nprint(f\"transformers: {transformers.__version__}\")\nprint(f\"datasets : {ds_lib.__version__}\")\nprint(\"All packages ready.\")\n"
|
| 38 |
-
]
|
| 39 |
-
},
|
| 40 |
-
{
|
| 41 |
-
"cell_type": "code",
|
| 42 |
-
"execution_count": null,
|
| 43 |
-
"id": "cell-config",
|
| 44 |
-
"metadata": {},
|
| 45 |
-
"outputs": [],
|
| 46 |
-
"source": [
|
| 47 |
-
"# ── Cell 3: CONFIGURATION — edit these before each run ───────────────────────\nimport os\nfrom pathlib import Path\n\n# ─── Environment detection (Kaggle / Colab / RunPod / local) ─────────────────\nif Path('/kaggle/working').exists():\n _ENV = 'kaggle'\n WORKING_DIR = '/kaggle/working'\nelif Path('/content').exists() and not Path('/workspace').exists():\n _ENV = 'colab'\n WORKING_DIR = '/content'\nelif Path('/workspace').exists():\n _ENV = 'runpod'\n WORKING_DIR = '/workspace'\nelse:\n _ENV = 'local'\n WORKING_DIR = os.environ.get('WORKING_DIR', os.path.expanduser('~/sahel-voice-work'))\n Path(WORKING_DIR).mkdir(parents=True, exist_ok=True)\n\n# ─── Language to train ───────────────────────────────────────────────────────\n# 'bam' = Bambara 'ful' = Fula\nTRAIN_LANG = os.environ.get('TRAIN_LANG', 'ful')\n\n# ─── Model ───────────────────────────────────────────────────────────────────\n# whisper-large-v3-turbo (128 mel bins) matches the Space base model.\n# On T4 (Kaggle, 16 GB) drop to 'openai/whisper-small' — turbo is tight there.\n_DEFAULT_MODEL = 'openai/whisper-small' if _ENV == 'kaggle' else 'openai/whisper-large-v3-turbo'\nWHISPER_MODEL_ID = os.environ.get('WHISPER_MODEL_ID', _DEFAULT_MODEL)\nTARGET_SR = 16_000\n\n# ─── HuggingFace repos ───────────────────────────────────────────────────────\nHF_USERNAME = os.environ.get('HF_USERNAME', 'ous-sow')\nFEEDBACK_REPO_ID = f'{HF_USERNAME}/sahel-agri-feedback'\nADAPTER_REPO_ID = f'{HF_USERNAME}/sahel-agri-adapters'\n\n# ─── Training hyper-parameters (defaults tuned per environment) ──────────────\nif _ENV == 'runpod':\n # 24 GB RTX 3090 / A40: turbo-v3 fits with batch 8, grad_accum 4 (eff. 32)\n MAX_STEPS = 2_000\n BATCH_SIZE = 8\n GRAD_ACCUM = 4\n MAX_WAXAL_TRAIN = 5_000\nelse:\n # T4 (Kaggle free) / local CPU fallback\n MAX_STEPS = 4_000\n BATCH_SIZE = 16\n GRAD_ACCUM = 2\n MAX_WAXAL_TRAIN = 5_000\n\nLEARNING_RATE = 1e-3\nWARMUP_STEPS = max(100, MAX_STEPS // 20)\nSAVE_STEPS = 500\nEVAL_STEPS = 500\nLOGGING_STEPS = 50\nCORRECTION_REPEAT= 3 # upsample user corrections Nx for emphasis\n\n# ─── Paths ────────────────────────────────────────────────────────────────────\nOUTPUT_DIR = f'{WORKING_DIR}/adapter_{TRAIN_LANG}'\nDATA_DIR = f'{WORKING_DIR}/data'\nAUDIO_DIR = f'{WORKING_DIR}/audio_feedback'\n\nLANG_NAME = {'bam': 'bambara', 'ful': 'fula'}.get(TRAIN_LANG, TRAIN_LANG)\nLANG_COUNTRY = {'bam': 'Mali', 'ful': 'Guinea'}.get(TRAIN_LANG, '')\nLANG_DIALECT = {\n 'bam': 'Standard Bambara (Bamako/Ségou) — Malian orthography',\n 'ful': 'Pular (Labé/Mamou dialects) — Guinean orthography',\n}.get(TRAIN_LANG, '')\n\nprint(f'Environment : {_ENV}')\nprint(f'Language : {TRAIN_LANG} ({LANG_NAME}) — {LANG_COUNTRY}')\nprint(f'Dialect : {LANG_DIALECT}')\nprint(f'Model : {WHISPER_MODEL_ID}')\nprint(f'Working dir : {WORKING_DIR}')\nprint(f'Output : {OUTPUT_DIR}')\nprint(f'Max steps : {MAX_STEPS} (batch={BATCH_SIZE}, grad_accum={GRAD_ACCUM}, eff={BATCH_SIZE*GRAD_ACCUM})')\n"
|
| 48 |
-
]
|
| 49 |
-
},
|
| 50 |
-
{
|
| 51 |
-
"cell_type": "code",
|
| 52 |
-
"execution_count": null,
|
| 53 |
-
"id": "cell-ext-config",
|
| 54 |
-
"metadata": {},
|
| 55 |
-
"outputs": [],
|
| 56 |
-
"source": [
|
| 57 |
-
"# -- Cell 4: External dataset configuration -----------------------------------\n# EXTERNAL_DATASETS is loaded dynamically from dataset_sources.jsonl in the\n# feedback repo. The Space's Self-Teaching tab writes dataset references there\n# when the user clicks \"Import from HuggingFace\". This cell reads that file\n# so any dataset registered in the Space is automatically used here.\n\nimport json as _json\nfrom huggingface_hub import hf_hub_download as _hf_dl\n\nEXTERNAL_DATASETS = []\n\n# -- Load dataset_sources.jsonl from Hub (written by Space Self-Teaching tab) --\ntry:\n _src_path = _hf_dl(\n repo_id=FEEDBACK_REPO_ID, filename='dataset_sources.jsonl',\n repo_type='dataset', token=HF_TOKEN,\n )\n with open(_src_path, encoding='utf-8') as _f:\n for _line in _f:\n _line = _line.strip()\n if not _line:\n continue\n _entry = _json.loads(_line)\n if not _entry.get('enabled'):\n continue\n # Normalise keys to what Cell 9 expects\n EXTERNAL_DATASETS.append({\n 'enabled' : True,\n 'repo_id' : _entry.get('repo', _entry.get('repo_id', '')),\n 'config' : _entry.get('config'),\n 'split' : _entry.get('split', 'train'),\n 'text_col' : _entry.get('text_col', 'transcription'),\n 'lang' : _entry.get('lang', _entry.get('language', TRAIN_LANG)),\n 'max_samples': _entry.get('max', _entry.get('max_samples', 2_000)),\n })\n print(f'dataset_sources.jsonl: loaded {len(EXTERNAL_DATASETS)} source(s)')\nexcept Exception as _e:\n print(f'dataset_sources.jsonl not found or empty ({_e}) -- using hardcoded list only')\n\nactive = [d for d in EXTERNAL_DATASETS if d.get('lang') == TRAIN_LANG]\nprint(f'External sources active for {TRAIN_LANG}: {len(active)}')\nfor _d in active:\n print(f\" - {_d['repo_id']} / {_d['config']} (max {_d['max_samples']} samples)\")\nif not active:\n if TRAIN_LANG == 'bam':\n print('Bambara: no external source yet.')\n print(' In the Space -> Self-Teaching tab -> Import from HuggingFace (Bambara).')\n elif TRAIN_LANG == 'ful':\n print('Fula: WaxalNLP ful_asr loaded in Cell 8 -- no extra source needed.')\n"
|
| 58 |
-
]
|
| 59 |
-
},
|
| 60 |
-
{
|
| 61 |
-
"cell_type": "code",
|
| 62 |
-
"execution_count": null,
|
| 63 |
-
"id": "cell-login",
|
| 64 |
-
"metadata": {},
|
| 65 |
-
"outputs": [],
|
| 66 |
-
"source": [
|
| 67 |
-
"# ── Cell 5: HuggingFace login + directory setup ───────────────────────────────\nimport os\nfrom pathlib import Path\n\nHF_TOKEN = None\n\n# Kaggle secrets (preferred)\ntry:\n from kaggle_secrets import UserSecretsClient # type: ignore\n HF_TOKEN = UserSecretsClient().get_secret('HF_TOKEN')\n print('HF_TOKEN loaded from Kaggle secrets.')\nexcept Exception:\n pass\n\n# Colab secrets (fallback)\nif not HF_TOKEN:\n try:\n from google.colab import userdata # type: ignore\n HF_TOKEN = userdata.get('HF_TOKEN')\n print('HF_TOKEN loaded from Colab secrets.')\n except Exception:\n pass\n\n# .env file (RunPod / local) - look in common locations\nif not HF_TOKEN:\n for _env_path in ['/workspace/sahel-voice/.env', './.env', '../.env', os.path.expanduser('~/.env')]:\n if os.path.isfile(_env_path):\n with open(_env_path, encoding='utf-8') as _f:\n for _line in _f:\n _line = _line.strip()\n if _line.startswith('HF_TOKEN='):\n _val = _line.split('=', 1)[1].strip()\n if _val and len(_val) >= 2 and _val[0] in ('\"', \"'\") and _val[-1] == _val[0]:\n _val = _val[1:-1]\n HF_TOKEN = _val\n print(f'HF_TOKEN loaded from {_env_path}')\n break\n if HF_TOKEN:\n break\n\nif not HF_TOKEN:\n HF_TOKEN = os.environ.get('HF_TOKEN', '')\n if HF_TOKEN:\n print('HF_TOKEN loaded from environment variable.')\n\nif not HF_TOKEN:\n raise ValueError(\n 'HF_TOKEN not found.\\n'\n 'Kaggle: Add-ons → Secrets → add HF_TOKEN → toggle \"Attach to notebook\" ON'\n )\n\nfrom huggingface_hub import login, HfApi\nlogin(token=HF_TOKEN, add_to_git_credential=False)\napi = HfApi(token=HF_TOKEN)\nos.environ['HF_TOKEN'] = HF_TOKEN\n\n# Create output directories\nfor d in [OUTPUT_DIR, DATA_DIR, AUDIO_DIR]:\n Path(d).mkdir(parents=True, exist_ok=True)\n\nprint(f'✅ Logged in | output: {OUTPUT_DIR}')"
|
| 68 |
-
]
|
| 69 |
-
},
|
| 70 |
-
{
|
| 71 |
-
"cell_type": "code",
|
| 72 |
-
"execution_count": null,
|
| 73 |
-
"id": "cell-resume",
|
| 74 |
-
"metadata": {},
|
| 75 |
-
"outputs": [],
|
| 76 |
-
"source": "# ── Cell 6: Resume-from-checkpoint detection ──────────────────────────────────\n# If OUTPUT_DIR already has checkpoints (e.g. Kaggle session timed out),\n# training will automatically resume from the latest one.\n#\n# NOTE: We do NOT use transformers.trainer_utils.get_last_checkpoint here.\n# That import pulls in peft → transformers.generation → masking_utils →\n# torch._dynamo before packages are settled, causing ImportError on Kaggle\n# Python 3.12. The function below replicates exactly what it does internally.\n\nimport re\nfrom pathlib import Path\n\ndef _get_last_checkpoint(folder: str):\n \"\"\"Return the highest-numbered checkpoint-N directory path, or None.\"\"\"\n p = Path(folder)\n if not p.exists():\n return None\n checkpoints = [\n d for d in p.iterdir()\n if d.is_dir() and re.fullmatch(r'checkpoint-\\d+', d.name)\n ]\n if not checkpoints:\n return None\n return str(max(checkpoints, key=lambda d: int(d.name.split('-')[1])))\n\n\nLAST_CHECKPOINT = _get_last_checkpoint(OUTPUT_DIR)\n\nif LAST_CHECKPOINT:\n print(f'⏩ Resume checkpoint found: {LAST_CHECKPOINT}')\n print(' Training will continue from this point.')\nelse:\n print('🆕 No checkpoint found — starting fresh training.')"
|
| 77 |
-
},
|
| 78 |
-
{
|
| 79 |
-
"cell_type": "code",
|
| 80 |
-
"execution_count": null,
|
| 81 |
-
"id": "cell-feedback",
|
| 82 |
-
"metadata": {},
|
| 83 |
-
"outputs": [],
|
| 84 |
-
"source": "# ── Cell 7: Download sahel-agri-feedback data ─────────────────────────────────\n# Downloads vocabulary.jsonl (word pairs) and corrections.jsonl (audio+text).\n# Audio files referenced in corrections.jsonl are also downloaded.\n\nimport json, shutil\nfrom pathlib import Path\nfrom huggingface_hub import hf_hub_download, list_repo_files\n\n# ── vocabulary.jsonl (word pairs taught by users) ──────────────────────────\nvocab_entries = []\ntry:\n vocab_path = hf_hub_download(\n repo_id=FEEDBACK_REPO_ID, filename='vocabulary.jsonl',\n repo_type='dataset', token=HF_TOKEN,\n )\n with open(vocab_path, encoding='utf-8') as f:\n vocab_entries = [json.loads(l) for l in f if l.strip()]\n print(f'vocabulary.jsonl : {len(vocab_entries)} entries')\nexcept Exception as e:\n print(f'vocabulary.jsonl not found or empty: {e}')\n\n# ── corrections.jsonl (audio corrections from the Space) ──────────────────\ncorrection_records = []\ntry:\n corr_path = hf_hub_download(\n repo_id=FEEDBACK_REPO_ID, filename='corrections.jsonl',\n repo_type='dataset', token=HF_TOKEN,\n )\n with open(corr_path, encoding='utf-8') as f:\n all_records = [json.loads(l) for l in f if l.strip()]\n correction_records = [\n r for r in all_records\n if r.get('language') == TRAIN_LANG\n and (r.get('corrected_text') or r.get('transcription'))\n and r.get('audio_file')\n ]\n print(f'corrections.jsonl: {len(all_records)} total, {len(correction_records)} for lang={TRAIN_LANG}')\nexcept Exception as e:\n print(f'corrections.jsonl not found or empty: {e}')\n\n# ── Download audio files referenced in corrections ─────────────────────────\nskipped_audio = 0\nfor rec in correction_records:\n audio_fname = Path(rec['audio_file']).name\n local_path = Path(AUDIO_DIR) / audio_fname\n if local_path.exists():\n rec['local_audio'] = str(local_path)\n continue\n try:\n dl = hf_hub_download(\n repo_id=FEEDBACK_REPO_ID, filename=rec['audio_file'],\n repo_type='dataset', token=HF_TOKEN,\n )\n shutil.copy2(dl, local_path)\n rec['local_audio'] = str(local_path)\n except Exception as e:\n skipped_audio += 1\n rec['local_audio'] = None\n\ncorrection_records = [r for r in correction_records if r.get('local_audio')]\nprint(f'Audio downloaded : {len(correction_records)} files ({skipped_audio} skipped)')\nprint(f'Vocab entries : {len(vocab_entries)}')"
|
| 85 |
-
},
|
| 86 |
-
{
|
| 87 |
-
"cell_type": "code",
|
| 88 |
-
"execution_count": null,
|
| 89 |
-
"id": "cell-waxal",
|
| 90 |
-
"metadata": {},
|
| 91 |
-
"outputs": [],
|
| 92 |
-
"source": "# -- Cell 8: Load WaxalNLP ----------------------------------------------------\n# WaxalNLP confirmed subsets (google/WaxalNLP dataset card):\n# Fula -> 'ful_asr' (available)\n# Bambara -> NOT present (no 'bam' config exists)\n#\n# google/fleurs is NOT used as a fallback -- datasets >= 3.0 refuses\n# to execute its legacy script loader (fleurs.py).\n#\n# Bambara training uses: user corrections (Cell 7) + Common Voice bm (Cell 4).\n# Fula training uses: user corrections + WaxalNLP ful_asr + Common Voice ff.\n\nfrom datasets import load_dataset, Audio as HFAudio\n\nWAXAL_SUBSET_MAP = {\n 'bam': None, # no Bambara subset in WaxalNLP -- skip silently\n 'ful': 'ful_asr', # confirmed available config\n}\n\nwaxal_ds = None\nWAXAL_TEXT_COL = 'transcription'\n\nsubset = WAXAL_SUBSET_MAP.get(TRAIN_LANG)\n\nif subset is None:\n print(f'WaxalNLP has no subset for lang={TRAIN_LANG} -- skipping.')\n print('Bambara will train on user corrections + Common Voice (Cell 4).')\nelse:\n try:\n print(f'Loading google/WaxalNLP subset={subset} (streaming) ...')\n waxal_ds = load_dataset(\n 'google/WaxalNLP', subset,\n split='train',\n streaming=True,\n token=HF_TOKEN,\n )\n\n # Probe first item to confirm schema\n probe = next(iter(waxal_ds))\n if 'audio' not in probe:\n raise ValueError(f'No audio column. Keys: {list(probe.keys())}')\n\n WAXAL_TEXT_COL = next(\n (k for k in ['transcription', 'text', 'sentence', 'normalized_text']\n if k in probe),\n None,\n )\n if WAXAL_TEXT_COL is None:\n raise ValueError(f'No text column. Keys: {list(probe.keys())}')\n\n print(f'WaxalNLP/{subset} ready -- text column: \"{WAXAL_TEXT_COL}\"')\n\n except Exception as e:\n print(f'WaxalNLP/{subset} failed: {e}')\n print('Continuing without WaxalNLP -- enable Common Voice in Cell 4.')\n waxal_ds = None\n\nstatus = f'WaxalNLP/{subset}' if waxal_ds is not None else 'not available'\nprint(f'\\nWaxal source for {TRAIN_LANG}: {status}')"
|
| 93 |
-
},
|
| 94 |
-
{
|
| 95 |
-
"cell_type": "code",
|
| 96 |
-
"execution_count": null,
|
| 97 |
-
"id": "cell-external",
|
| 98 |
-
"metadata": {},
|
| 99 |
-
"outputs": [],
|
| 100 |
-
"source": "# ── Cell 9: Load external datasets (from Cell 4 config) ──────────────────────\nfrom datasets import load_dataset, Audio as HFAudio\n\nexternal_datasets = [] # list of (hf_dataset, text_col)\n\nfor cfg in EXTERNAL_DATASETS:\n if not cfg['enabled'] or cfg['lang'] != TRAIN_LANG:\n continue\n try:\n print(f'Loading {cfg[\"repo_id\"]} / {cfg[\"config\"]} ...')\n ds = load_dataset(\n cfg['repo_id'], cfg['config'],\n split=cfg['split'],\n streaming=True,\n token=HF_TOKEN,\n )\n probe = next(iter(ds))\n text_col = cfg['text_col'] if cfg['text_col'] in probe else next(\n (k for k in ['transcription', 'text', 'sentence'] if k in probe), None\n )\n if text_col is None:\n print(f' ⚠️ Cannot find text column — skipping')\n continue\n if 'audio' not in probe:\n print(f' ⚠️ No audio column — skipping')\n continue\n # Cap at max_samples\n ds = ds.take(cfg.get('max_samples', 2_000))\n external_datasets.append((ds, text_col))\n print(f' ✅ {cfg[\"repo_id\"]} — text col \"{text_col}\", max {cfg.get(\"max_samples\",2000)} samples')\n except Exception as e:\n print(f' ⚠️ {cfg[\"repo_id\"]} failed: {e}')\n\nprint(f'\\nExternal sources loaded: {len(external_datasets)}')"
|
| 101 |
-
},
|
| 102 |
-
{
|
| 103 |
-
"cell_type": "markdown",
|
| 104 |
-
"id": "md-pipeline",
|
| 105 |
-
"metadata": {},
|
| 106 |
-
"source": "---\n## Data Pipeline\n\nAll audio is resampled to 16 kHz. Text is cleaned with a language-aware allowlist that keeps Latin script extended characters valid for Bambara (ɛ ɔ ŋ) and Fula (ɓ ɗ ƴ ŋ ɲ), stripping everything else (URLs, XML tags, symbols). The Whisper processor converts the cleaned text to token IDs."
|
| 107 |
-
},
|
| 108 |
-
{
|
| 109 |
-
"cell_type": "code",
|
| 110 |
-
"execution_count": null,
|
| 111 |
-
"id": "cell-clean",
|
| 112 |
-
"metadata": {},
|
| 113 |
-
"outputs": [],
|
| 114 |
-
"source": [
|
| 115 |
-
"# -- Cell 10: Text cleaning utilities + Bambara phonetic normaliser -----------\nimport re, unicodedata\n\n# Phonetic normaliser: unifies French-influenced spellings before training.\n# ou->u, dj->j, gn->ny_palatal etc. so spelling variants map to same token.\n_BAM_NORM_RULES = [('ou','u'),('dj','j'),('gn','ɲ'),('ny','ɲ'),('ch','c'),('oo','ɔ'),('ee','ɛ')]\n_BAM_NORM_PAT = re.compile('|'.join(re.escape(s) for s,_ in _BAM_NORM_RULES))\n_BAM_NORM_MAP = {s:d for s,d in _BAM_NORM_RULES}\n\ndef _bam_norm(text):\n import unicodedata as _ud\n text = _ud.normalize('NFC', text.lower())\n return _BAM_NORM_PAT.sub(lambda m: _BAM_NORM_MAP[m.group(0)], text)\n\n# Pular (Fula of Guinea) normaliser: converts Adlam script → Latin,\n# then NFC + lowercase. Needed because guizme/adlam_fulfulde labels are in\n# Adlam (U+1E900-U+1E95F) which Whisper’s tokenizer has no coverage for.\n_ADLAM_TO_LATIN = [\n (\"𞤀\",\"A\"),(\"𞤁\",\"B\"),(\"𞤂\",\"B\"),(\"𞤃\",\"D\"),(\"𞤄\",\"D\"),\n (\"𞤅\",\"E\"),(\"𞤆\",\"F\"),(\"𞤇\",\"G\"),(\"𞤈\",\"H\"),(\"𞤉\",\"I\"),\n (\"𞤊\",\"J\"),(\"𞤋\",\"K\"),(\"𞤌\",\"L\"),(\"𞤍\",\"M\"),(\"𞤎\",\"N\"),\n (\"𞤏\",\"NG\"),(\"𞤐\",\"O\"),(\"𞤑\",\"P\"),(\"𞤒\",\"R\"),(\"𞤓\",\"S\"),\n (\"𞤔\",\"T\"),(\"𞤕\",\"U\"),(\"𞤖\",\"V\"),(\"𞤗\",\"W\"),(\"𞤘\",\"Y\"),\n (\"𞤙\",\"Z\"),(\"𞤚\",\"KH\"),(\"𞤛\",\"QU\"),(\"𞤜\",\"SH\"),(\"𞤝\",\"GH\"),\n (\"𞤞\",\"NY\"),(\"𞤟\",\"TH\"),(\"𞤠\",\"WH\"),(\"𞤡\",\"NY\"),\n (\"𞤢\",\"a\"),(\"𞤣\",\"b\"),(\"𞤤\",\"b\"),(\"𞤥\",\"d\"),(\"𞤦\",\"d\"),\n (\"𞤧\",\"e\"),(\"𞤨\",\"f\"),(\"𞤩\",\"g\"),(\"𞤪\",\"h\"),(\"𞤫\",\"i\"),\n (\"𞤬\",\"j\"),(\"𞤭\",\"k\"),(\"𞤮\",\"l\"),(\"𞤯\",\"m\"),(\"𞤰\",\"n\"),\n (\"𞤱\",\"ng\"),(\"𞤲\",\"o\"),(\"𞤳\",\"p\"),(\"𞤴\",\"r\"),(\"𞤵\",\"s\"),\n (\"𞤶\",\"t\"),(\"𞤷\",\"u\"),(\"𞤸\",\"v\"),(\"𞤹\",\"w\"),(\"𞤺\",\"y\"),\n (\"𞤻\",\"z\"),(\"𞤼\",\"kh\"),(\"𞤽\",\"qu\"),(\"𞤾\",\"sh\"),(\"𞤿\",\"gh\"),\n (\"𞥀\",\"ny\"),(\"𞥁\",\"th\"),(\"𞥂\",\"wh\"),(\"𞥃\",\"ny\"),\n]\n_A2L = {a: l for a, l in _ADLAM_TO_LATIN}\n_ADLAM_START, _ADLAM_END = 0x1E900, 0x1E95F\n\ndef _contains_adlam(text):\n return any(_ADLAM_START <= ord(c) <= _ADLAM_END for c in text)\n\ndef _normalize_pular(text):\n import unicodedata as _ud, re as _re\n if _contains_adlam(text):\n text = \"\".join(_A2L.get(c, c) for c in text)\n text = _ud.normalize(\"NFC\", text.lower())\n return _re.sub(r\"\\s+\", \" \", text).strip()\n\n\n_BAMBARA_EXTRA = {'\\u025b','\\u0254','\\u014b'}\n_FULA_EXTRA = {'\\u0253','\\u0257','\\u01b4','\\u014b','\\u0272'}\n_BASE_LATIN = set('abcdefghijklmnopqrstuvwxyz')\n_ACCENTED = set('\\u00e0\\u00e2\\u00e4\\u00e8\\u00e9\\u00ea\\u00eb'\n '\\u00ee\\u00ef\\u00f4\\u00f9\\u00fb\\u00fc\\u00fd'\n '\\u00ff\\u00e6\\u0153\\u00e7')\n_KEEP_PUNCT = set(\" ',-.'!?\")\n\n_VALID_CHARS = {\n 'bam': _BASE_LATIN | _ACCENTED | _BAMBARA_EXTRA | _KEEP_PUNCT,\n 'ful': _BASE_LATIN | _ACCENTED | _FULA_EXTRA | _KEEP_PUNCT,\n}\n\n\ndef clean_text(text: str, lang: str = 'bam') -> str:\n if not text:\n return ''\n text = unicodedata.normalize('NFKC', text.lower().strip())\n text = re.sub(r'https?://\\S+', '', text)\n text = re.sub(r'<[^>]+>', '', text)\n text = re.sub(r'([.,!?])\\1+', r'\\1', text)\n valid = _VALID_CHARS.get(lang, _VALID_CHARS['bam'] | _VALID_CHARS['ful'])\n text = ''.join(c for c in text if c in valid)\n return re.sub(r'\\s+', ' ', text).strip()\n\n\n# Verify actual output then assert against it\nr1 = clean_text('I ni ce! (hello)', 'bam') # parens stripped, ! kept\nr2 = clean_text('Jam waali. <b>test</b>', 'ful') # tags stripped, content kept\nr3 = clean_text('Visit https://example.com now!!', 'bam') # URL stripped, word before stays\n\nassert r1 == 'i ni ce! hello', f'r1: {repr(r1)}'\nassert r2 == 'jam waali. test', f'r2: {repr(r2)}'\nassert r3 == 'visit now!', f'r3: {repr(r3)}'\n\nprint('clean_text tests passed')\nprint(f' {repr(r1)}')\nprint(f' {repr(r2)}')\nprint(f' {repr(r3)}')"
|
| 116 |
-
]
|
| 117 |
-
},
|
| 118 |
-
{
|
| 119 |
-
"cell_type": "code",
|
| 120 |
-
"execution_count": null,
|
| 121 |
-
"id": "cell-prepare",
|
| 122 |
-
"metadata": {},
|
| 123 |
-
"outputs": [],
|
| 124 |
-
"source": [
|
| 125 |
-
"# -- Cell 11: Whisper processor + prepare_dataset -----------------------------\n# WhisperProcessor imports processing_utils -> image_utils -> torchvision,\n# which crashes when torch/torchvision have mismatched CUDA versions.\n# Fix: build the processor manually from its two sub-components.\n# WhisperFeatureExtractor and WhisperTokenizer have no torchvision dependency.\nimport numpy as np\n\nfrom transformers.models.whisper.feature_extraction_whisper import WhisperFeatureExtractor\nfrom transformers.models.whisper.tokenization_whisper import WhisperTokenizer\n\nprint(f'Loading Whisper feature extractor + tokenizer: {WHISPER_MODEL_ID} ...')\n_feat_ext = WhisperFeatureExtractor.from_pretrained(WHISPER_MODEL_ID, token=HF_TOKEN)\n_tokenizer = WhisperTokenizer.from_pretrained(WHISPER_MODEL_ID, token=HF_TOKEN)\n\n\nclass _Processor:\n \"\"\"Minimal WhisperProcessor substitute that avoids the torchvision import chain.\"\"\"\n def __init__(self, feature_extractor, tokenizer):\n self.feature_extractor = feature_extractor\n self.tokenizer = tokenizer\n\n def get_decoder_prompt_ids(self, language, task='transcribe'):\n return self.tokenizer.get_decoder_prompt_ids(language=language, task=task)\n\n def save_pretrained(self, path):\n self.feature_extractor.save_pretrained(path)\n self.tokenizer.save_pretrained(path)\n\n\nprocessor = _Processor(_feat_ext, _tokenizer)\nprint('Processor ready')\n\n\ndef prepare_dataset(batch, text_col='transcription', lang=TRAIN_LANG):\n \"\"\"\n Resample to 16 kHz, extract log-mel features, tokenise text.\n Works on any dict with 'audio' (HF Audio column) and a text column.\n \"\"\"\n audio = batch['audio']\n audio_array = np.array(audio['array'], dtype=np.float32)\n orig_sr = audio['sampling_rate']\n\n if orig_sr != TARGET_SR:\n try:\n import torchaudio.functional as F_audio, torch\n audio_array = F_audio.resample(\n torch.from_numpy(audio_array).unsqueeze(0),\n orig_sr, TARGET_SR,\n ).squeeze(0).numpy()\n except Exception:\n import librosa\n audio_array = librosa.resample(audio_array, orig_sr=orig_sr, target_sr=TARGET_SR)\n\n batch['input_features'] = processor.feature_extractor(\n audio_array, sampling_rate=TARGET_SR\n ).input_features[0]\n\n raw_text = batch.get(text_col, '') or ''\n _norm_text = _bam_norm(str(raw_text)) if lang == 'bam' else (_normalize_pular(str(raw_text)) if lang == 'ful' else str(raw_text))\n cleaned = clean_text(_norm_text, lang=lang)\n batch['labels'] = processor.tokenizer(cleaned).input_ids\n return batch\n\n\nprint('prepare_dataset ready')"
|
| 126 |
-
]
|
| 127 |
-
},
|
| 128 |
-
{
|
| 129 |
-
"cell_type": "code",
|
| 130 |
-
"execution_count": null,
|
| 131 |
-
"id": "cell-merge",
|
| 132 |
-
"metadata": {},
|
| 133 |
-
"outputs": [],
|
| 134 |
-
"source": [
|
| 135 |
-
"# -- Cell 12: Build & merge all datasets --------------------------------------\nfrom datasets import Dataset, Audio as HFAudio, concatenate_datasets\nfrom functools import partial\n\ntrain_ds = None # set here so Cell 12b can detect whether we succeeded\neval_ds = None\nall_parts = []\n\n# -- Part A: User corrections -------------------------------------------------\nif correction_records:\n print(f'Part A: {len(correction_records)} corrections x {CORRECTION_REPEAT}')\n rec_list = correction_records * CORRECTION_REPEAT\n corr_ds = Dataset.from_dict({\n 'audio': [r['local_audio'] for r in rec_list],\n 'transcription': [r.get('corrected_text') or r.get('transcription', '') for r in rec_list],\n })\n corr_ds = corr_ds.cast_column('audio', HFAudio(sampling_rate=TARGET_SR))\n corr_ds = corr_ds.map(\n partial(prepare_dataset, text_col='transcription'),\n remove_columns=corr_ds.column_names,\n )\n all_parts.append(corr_ds)\n print(f' -> {len(corr_ds)} samples')\nelse:\n print('Part A: no corrections -- skipping')\n\n# -- Part B: WaxalNLP ---------------------------------------------------------\nif waxal_ds is not None:\n print(f'Part B: materialising up to {MAX_WAXAL_TRAIN} WaxalNLP samples ...')\n waxal_rows = list(waxal_ds.take(MAX_WAXAL_TRAIN))\n waxal_local = Dataset.from_dict({\n 'audio': [r['audio'] for r in waxal_rows],\n 'transcription': [r.get(WAXAL_TEXT_COL, '') for r in waxal_rows],\n })\n waxal_local = waxal_local.cast_column('audio', HFAudio(sampling_rate=TARGET_SR))\n waxal_local = waxal_local.map(\n partial(prepare_dataset, text_col='transcription'),\n remove_columns=waxal_local.column_names,\n )\n all_parts.append(waxal_local)\n print(f' -> {len(waxal_local)} samples')\nelse:\n print('Part B: WaxalNLP not available -- skipping')\n\n# -- Part C: External datasets ------------------------------------------------\nfor ext_ds, text_col in external_datasets:\n ext_rows = list(ext_ds)\n if not ext_rows:\n continue\n ext_local = Dataset.from_dict({\n 'audio': [r['audio'] for r in ext_rows],\n 'transcription': [r.get(text_col, '') for r in ext_rows],\n })\n ext_local = ext_local.cast_column('audio', HFAudio(sampling_rate=TARGET_SR))\n ext_local = ext_local.map(\n partial(prepare_dataset, text_col='transcription'),\n remove_columns=ext_local.column_names,\n )\n all_parts.append(ext_local)\n print(f'Part C: {len(ext_local)} external samples')\n\n# -- Result -------------------------------------------------------------------\nprint(f'\\nData summary: {len(all_parts)} source(s) loaded')\n\nif not all_parts:\n print('No real data available for Bambara yet.')\n print('Cell 12b (below) will build a synthetic dataset so the full')\n print('training pipeline can be validated. Switch to TRAIN_LANG=\"ful\"')\n print('in Cell 3 for a real Fula training run using WaxalNLP.')\nelse:\n combined = concatenate_datasets(all_parts).shuffle(seed=42)\n n_eval = max(1, min(int(0.05 * len(combined)), 200))\n split = combined.train_test_split(test_size=n_eval)\n train_ds = split['train']\n eval_ds = split['test']\n print(f'Train: {len(train_ds)} Eval: {len(eval_ds)}')\n# Auto-cap MAX_STEPS: no point running 4000 steps on a tiny dataset.\n# Rule: at least 20 passes through the data, capped at user's MAX_STEPS.\nif train_ds is not None:\n steps_per_epoch = max(1, len(train_ds) // (BATCH_SIZE * GRAD_ACCUM))\n _auto_steps = max(200, steps_per_epoch * 20)\n if _auto_steps < MAX_STEPS:\n print(f'Auto-capping MAX_STEPS {MAX_STEPS} -> {_auto_steps} (small dataset)')\n MAX_STEPS = _auto_steps\n else:\n print(f'MAX_STEPS={MAX_STEPS} OK for {len(train_ds)} training samples')"
|
| 136 |
-
]
|
| 137 |
-
},
|
| 138 |
-
{
|
| 139 |
-
"cell_type": "code",
|
| 140 |
-
"id": "5beb513a",
|
| 141 |
-
"source": "# -- Cell 12b: Synthetic fallback (SKIP if Cell 12 succeeded) ----------------\n# Run this cell ONLY if Cell 12 raised \"No data loaded\".\n# Generates 50 short silent audio samples labelled with vocabulary.jsonl\n# entries so training can proceed and you can verify the pipeline works.\n# Replace with real data (accept Common Voice terms, or add corrections) for\n# a meaningful model.\n\nimport numpy as np\nfrom datasets import Dataset, Audio as HFAudio, concatenate_datasets\nfrom functools import partial\n\nif 'train_ds' in dir() and train_ds is not None:\n print('Cell 12 succeeded -- nothing to do here.')\nelse:\n print('Building synthetic fallback dataset from vocabulary.jsonl ...')\n\n # Use vocab entries if available, otherwise generic phrases\n if vocab_entries:\n phrases = [e.get('word', 'test') for e in vocab_entries[:50]]\n else:\n phrases = [f'word {i}' for i in range(50)]\n\n SR = TARGET_SR\n rows = []\n for phrase in phrases:\n # 1-second silent audio (safe baseline for feature extraction)\n audio_array = np.zeros(SR, dtype=np.float32)\n rows.append({'audio_array': audio_array, 'transcription': phrase})\n\n # Build dataset directly from numpy arrays\n synth_ds = Dataset.from_dict({\n 'transcription': [r['transcription'] for r in rows],\n })\n\n # Add audio column manually\n def _add_audio(batch, idx):\n batch['input_features'] = processor.feature_extractor(\n np.zeros(TARGET_SR, dtype=np.float32), sampling_rate=TARGET_SR\n ).input_features[0]\n cleaned = clean_text(rows[idx]['transcription'], lang=TRAIN_LANG)\n batch['labels'] = processor.tokenizer(cleaned).input_ids\n return batch\n\n synth_processed = synth_ds.map(\n _add_audio,\n with_indices=True,\n remove_columns=synth_ds.column_names,\n )\n\n split = synth_processed.train_test_split(test_size=0.1, seed=42)\n train_ds = split['train']\n eval_ds = split['test']\n print(f'Synthetic fallback: {len(train_ds)} train, {len(eval_ds)} eval')\n print('WARNING: training on synthetic data produces a non-functional model.')\n print('Accept Common Voice terms and re-run Cell 9 + Cell 12 for real data.')",
|
| 142 |
-
"metadata": {},
|
| 143 |
-
"execution_count": null,
|
| 144 |
-
"outputs": []
|
| 145 |
-
},
|
| 146 |
-
{
|
| 147 |
-
"cell_type": "markdown",
|
| 148 |
-
"id": "md-model",
|
| 149 |
-
"metadata": {},
|
| 150 |
-
"source": [
|
| 151 |
-
"---\n## Model Setup - Partial Freeze Fine-Tuning\n\nopenai/whisper-small is loaded in fp16. All parameters are frozen, then\nthe last 2 decoder layers + layer norm + output projection are unfrozen (~5%\ntrainable params). No PEFT/LoRA -- avoids PEFT/transformers 5.x\nincompatibility where input_ids is passed twice to WhisperDecoder.\n"
|
| 152 |
-
]
|
| 153 |
-
},
|
| 154 |
-
{
|
| 155 |
-
"cell_type": "code",
|
| 156 |
-
"execution_count": null,
|
| 157 |
-
"id": "cell-model",
|
| 158 |
-
"metadata": {},
|
| 159 |
-
"outputs": [],
|
| 160 |
-
"source": [
|
| 161 |
-
"# -- Cell 13: Load Whisper-small (fp16) + freeze most layers ------------------\n# PEFT LoRA causes TypeError with transformers 5.x regardless of which layers\n# are targeted: PeftModelForSeq2SeqLM wraps the entire model in BaseTuner whose\n# forward(*args, **kwargs) passes input_ids in a way that causes WhisperDecoder\n# to receive it twice. Fix: skip PEFT entirely. Freeze all params, unfreeze\n# last 2 decoder layers (~5% trainable params -- same capacity as LoRA r=32).\nimport torch\nfrom transformers.models.whisper.modeling_whisper import WhisperForConditionalGeneration\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Loading {WHISPER_MODEL_ID} in fp32 on {device} ...\")\n\nmodel = WhisperForConditionalGeneration.from_pretrained(\n WHISPER_MODEL_ID,\n torch_dtype=torch.float32, # fp32 storage -- AMP casts internally during training\n token=HF_TOKEN,\n)\nmodel = model.to(device)\n\n# Force target language -- avoids language-detection overhead during training\n# Move generation params to GenerationConfig (avoids deprecation warning)\n_dec_ids = processor.get_decoder_prompt_ids(language='fr', task='transcribe')\nmodel.generation_config.forced_decoder_ids = _dec_ids\nmodel.generation_config.suppress_tokens = []\nmodel.config.use_cache = False # required for gradient checkpointing\n\n# ── Freeze all params, then selectively unfreeze ─────────────────────────────\nfor param in model.parameters():\n param.requires_grad = False\n\n# Unfreeze last 2 decoder layers + final layer norm + output projection.\n# These handle language-specific token generation.\nfor module in [\n model.model.decoder.layers[-2],\n model.model.decoder.layers[-1],\n model.model.decoder.layer_norm,\n model.proj_out,\n]:\n for param in module.parameters():\n param.requires_grad = True\n\ntrainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\ntotal = sum(p.numel() for p in model.parameters())\nprint(f\"Trainable params: {trainable:,} / {total:,} ({100*trainable/total:.1f}%)\")\n\n# gradient_checkpointing enabled via TrainingArguments below (args handle enable/disable)\nmodel.train()\n\nvram_mb = torch.cuda.memory_allocated() / 1e6 if torch.cuda.is_available() else 0\ntotal_vram = torch.cuda.get_device_properties(0).total_memory / 1e6 if torch.cuda.is_available() else 0\nprint(f\"VRAM used: {vram_mb:.0f} MB / {total_vram:.0f} MB\")\nprint(f\"Model ready on {device}\")\n"
|
| 162 |
-
]
|
| 163 |
-
},
|
| 164 |
-
{
|
| 165 |
-
"cell_type": "code",
|
| 166 |
-
"execution_count": null,
|
| 167 |
-
"id": "cell-collator",
|
| 168 |
-
"metadata": {},
|
| 169 |
-
"outputs": [],
|
| 170 |
-
"source": [
|
| 171 |
-
"# -- Cell 14: Data collator + CER metric --------------------------------------\nimport jiwer\nfrom dataclasses import dataclass\nfrom typing import Any, Dict, List\n\ntransform = jiwer.Compose([\n jiwer.ToLowerCase(),\n jiwer.RemoveMultipleSpaces(),\n jiwer.Strip(),\n jiwer.RemovePunctuation(),\n jiwer.ReduceToListOfListOfWords(),\n])\n\n# CER transform (no word-split step needed)\n_cer_transform = jiwer.Compose([\n jiwer.ToLowerCase(),\n jiwer.RemoveMultipleSpaces(),\n jiwer.Strip(),\n jiwer.RemovePunctuation(),\n])\n\n\n@dataclass\nclass DataCollatorSpeechSeq2SeqWithPadding:\n processor: Any\n\n def __call__(self, features: List[Dict]) -> Dict:\n import torch\n input_feats = [{'input_features': f['input_features']} for f in features]\n batch = self.processor.feature_extractor.pad(input_feats, return_tensors='pt')\n\n # Leave features in fp32 -- AMP (fp16=True in TrainingArgs) handles casting\n\n label_feats = [{'input_ids': f['labels']} for f in features]\n labels_batch = self.processor.tokenizer.pad(label_feats, return_tensors='pt')\n labels = labels_batch['input_ids'].masked_fill(\n labels_batch.attention_mask.ne(1), -100\n )\n if (labels[:, 0] == self.processor.tokenizer.bos_token_id).all().item():\n labels = labels[:, 1:]\n batch['labels'] = labels\n return batch\n\n\ndef _apply_jiwer_transform(texts, t):\n \"\"\"Apply a jiwer Compose transform and return plain strings (not nested lists).\"\"\"\n import re as _re\n result = []\n for s in texts:\n s = s.lower()\n s = _re.sub(r'[^\\w\\s]', '', s) # RemovePunctuation equivalent\n s = ' '.join(s.split()) # RemoveMultipleSpaces + Strip\n result.append(s)\n return result\n\n\ndef compute_metrics(pred):\n pred_ids = pred.predictions\n label_ids = pred.label_ids\n label_ids[label_ids == -100] = processor.tokenizer.pad_token_id\n\n pred_str = processor.tokenizer.batch_decode(pred_ids, skip_special_tokens=True)\n label_str = processor.tokenizer.batch_decode(label_ids, skip_special_tokens=True)\n\n # Pre-normalise so we can filter empties AFTER the transform.\n # A reference like \"?\" or \"1\" decodes to non-empty but becomes empty\n # after punctuation/number removal -- jiwer crashes on empty references.\n norm_ref = _apply_jiwer_transform(label_str, _cer_transform)\n norm_hyp = _apply_jiwer_transform(pred_str, _cer_transform)\n pairs = [(r, h) for r, h in zip(norm_ref, norm_hyp) if r.strip()]\n if not pairs:\n return {'cer': 0.0, 'wer': 0.0}\n ref_clean, hyp_clean = zip(*pairs)\n\n cer = jiwer.cer(list(ref_clean), list(hyp_clean)) # already normalised\n wer = jiwer.wer(list(ref_clean), list(hyp_clean)) # already normalised\n return {'cer': round(cer, 4), 'wer': round(wer, 4)}\n\n\ncollator = DataCollatorSpeechSeq2SeqWithPadding(processor=processor)\nprint('Collator and WER metric ready')"
|
| 172 |
-
]
|
| 173 |
-
},
|
| 174 |
-
{
|
| 175 |
-
"cell_type": "markdown",
|
| 176 |
-
"id": "md-train",
|
| 177 |
-
"metadata": {},
|
| 178 |
-
"source": "---\n## Training — Deep Sleep 😴\n\nThe trainer runs for `MAX_STEPS` steps, saving a checkpoint every `SAVE_STEPS`.\nIf the Kaggle session times out and you re-run the notebook, Cell 6 will detect\nthe latest checkpoint and `trainer.train()` will resume automatically — no progress lost."
|
| 179 |
-
},
|
| 180 |
-
{
|
| 181 |
-
"cell_type": "code",
|
| 182 |
-
"execution_count": null,
|
| 183 |
-
"id": "cell-train-args",
|
| 184 |
-
"metadata": {},
|
| 185 |
-
"outputs": [],
|
| 186 |
-
"source": [
|
| 187 |
-
"# -- Cell 15: Training arguments ----------------------------------------------\nimport inspect\nfrom transformers import Seq2SeqTrainingArguments\n\n# transformers 4.x used 'evaluation_strategy'; 4.45+ renamed to 'eval_strategy'.\n# Detect which name this installed version accepts.\n_params = inspect.signature(Seq2SeqTrainingArguments.__init__).parameters\n_eval_key = 'eval_strategy' if 'eval_strategy' in _params else 'evaluation_strategy'\n\ntraining_args = Seq2SeqTrainingArguments(\n output_dir=OUTPUT_DIR,\n\n max_steps=MAX_STEPS,\n warmup_steps=WARMUP_STEPS,\n logging_steps=LOGGING_STEPS,\n save_steps=SAVE_STEPS,\n eval_steps=EVAL_STEPS,\n\n per_device_train_batch_size=BATCH_SIZE,\n per_device_eval_batch_size=8,\n gradient_accumulation_steps=GRAD_ACCUM,\n\n fp16=True,\n gradient_checkpointing=True, # reduces activation memory on T4\n\n learning_rate=LEARNING_RATE,\n lr_scheduler_type='cosine',\n weight_decay=0.0,\n adam_beta1=0.9,\n adam_beta2=0.98,\n adam_epsilon=1e-6,\n\n **{_eval_key: 'steps'},\n predict_with_generate=True,\n generation_max_length=225,\n load_best_model_at_end=True,\n metric_for_best_model='cer',\n greater_is_better=False,\n\n save_total_limit=3,\n save_strategy='steps',\n\n report_to=['tensorboard'], # tensorboard logs to OUTPUT_DIR/runs by default\n push_to_hub=False,\n)\n\nprint(f'Training arguments ready (using {_eval_key}=steps)')\nprint(f' Effective batch size: {BATCH_SIZE * GRAD_ACCUM}')\nprint(f' Max steps : {MAX_STEPS}')\n"
|
| 188 |
-
]
|
| 189 |
-
},
|
| 190 |
-
{
|
| 191 |
-
"cell_type": "code",
|
| 192 |
-
"execution_count": null,
|
| 193 |
-
"id": "cell-train",
|
| 194 |
-
"metadata": {},
|
| 195 |
-
"outputs": [],
|
| 196 |
-
"source": [
|
| 197 |
-
"# -- Cell 16: TRAIN -----------------------------------------------------------\nfrom transformers import Seq2SeqTrainer\n\ntrainer = Seq2SeqTrainer(\n model=model,\n args=training_args,\n train_dataset=train_ds,\n eval_dataset=eval_ds,\n data_collator=collator,\n compute_metrics=compute_metrics,\n # 'tokenizer' argument removed -- renamed to 'processing_class' in\n # transformers 5.x and passing the old name raises TypeError\n)\n\nprint(f'Starting training ...')\nprint(f' Resume from : {LAST_CHECKPOINT or \"scratch\"}')\nprint(f' Train size : {len(train_ds)}')\nprint(f' Eval size : {len(eval_ds)}')\n\ntrain_result = trainer.train(resume_from_checkpoint=LAST_CHECKPOINT)\n\nprint('\\nTraining complete')\nprint(f' Steps : {train_result.global_step}')\nprint(f' Train loss: {train_result.training_loss:.4f}')\n\ntrainer.save_model(OUTPUT_DIR)\nmodel.generation_config.save_pretrained(OUTPUT_DIR)\nprocessor.save_pretrained(OUTPUT_DIR)\nprint(f' Adapter saved -> {OUTPUT_DIR}')"
|
| 198 |
-
]
|
| 199 |
-
},
|
| 200 |
-
{
|
| 201 |
-
"cell_type": "markdown",
|
| 202 |
-
"id": "md-eval",
|
| 203 |
-
"metadata": {},
|
| 204 |
-
"source": "---\n## Evaluation\n\nWER (Word Error Rate) is computed on the held-out eval split.\nA lower WER means fewer transcription mistakes. For Bambara/Fula on whisper-small,\na WER < 40% is a strong result given the limited training data."
|
| 205 |
-
},
|
| 206 |
-
{
|
| 207 |
-
"cell_type": "code",
|
| 208 |
-
"execution_count": null,
|
| 209 |
-
"id": "cell-eval",
|
| 210 |
-
"metadata": {},
|
| 211 |
-
"outputs": [],
|
| 212 |
-
"source": [
|
| 213 |
-
"# ── Cell 17: WER evaluation ───────────────────────────────────────────────────\nprint('Running full evaluation on eval split ...')\neval_results = trainer.evaluate()\n\ncer_score = eval_results.get('eval_cer', float('nan'))\nwer_score = eval_results.get('eval_wer', float('nan'))\nprint(f'\\n✅ Final CER : {cer_score:.1%} (primary — lower is better)')\nprint(f' Final WER : {wer_score:.1%} (secondary)')\nprint(f' Eval loss : {eval_results.get(\"eval_loss\", float(\"nan\")):.4f}')\n# Show a few example transcriptions side-by-side\nimport random, torch\nprint('\\n── Sample predictions ───────────────────────────────')\nsamples = random.sample(range(len(eval_ds)), min(5, len(eval_ds)))\nfor idx in samples:\n item = eval_ds[idx]\n feats = torch.tensor(item['input_features']).unsqueeze(0).to(model.device)\n with torch.no_grad():\n pred_ids = model.generate(\n feats, # fp32 to match model dtype\n max_new_tokens=128,\n )\n pred_str = processor.tokenizer.batch_decode(pred_ids, skip_special_tokens=True)[0]\n labels = [t if t != -100 else processor.tokenizer.pad_token_id\n for t in item['labels']]\n ref_str = processor.tokenizer.decode(labels, skip_special_tokens=True)\n print(f' Ref : {ref_str}')\n print(f' Pred: {pred_str}')\n print()"
|
| 214 |
-
]
|
| 215 |
-
},
|
| 216 |
-
{
|
| 217 |
-
"cell_type": "markdown",
|
| 218 |
-
"id": "md-export",
|
| 219 |
-
"metadata": {},
|
| 220 |
-
"source": [
|
| 221 |
-
"---\n## Export — Push Fine-tuned Checkpoint to Hub\n\nThe adapter is pushed to `ous-sow/sahel-agri-adapters` under the path \n`adapters/{lang_name}/` with a Git tag like `v1.2-bambara`.\n\nThe version number is **auto-incremented** by reading existing tags on the repo\nso each training run gets a unique, traceable identifier."
|
| 222 |
-
]
|
| 223 |
-
},
|
| 224 |
-
{
|
| 225 |
-
"cell_type": "code",
|
| 226 |
-
"execution_count": null,
|
| 227 |
-
"id": "cell-version",
|
| 228 |
-
"metadata": {},
|
| 229 |
-
"outputs": [],
|
| 230 |
-
"source": "# ── Cell 18: Compute next version tag ────────────────────────────────────────\nimport re as _re\nfrom huggingface_hub import list_repo_refs\n\ndef get_next_version_tag(repo_id: str, lang_name: str, hf_token: str) -> str:\n \"\"\"Auto-increment version tag: reads existing tags, bumps minor version.\"\"\"\n try:\n refs = list_repo_refs(repo_id, repo_type='model', token=hf_token)\n pattern = _re.compile(rf'^v(\\d+)\\.(\\d+)-{_re.escape(lang_name)}$')\n versions = []\n for tag in refs.tags:\n m = pattern.match(tag.name)\n if m:\n versions.append((int(m.group(1)), int(m.group(2))))\n if not versions:\n return f'v1.0-{lang_name}'\n major, minor = max(versions)\n return f'v{major}.{minor + 1}-{lang_name}'\n except Exception as e:\n print(f' Could not read existing tags ({e}) — defaulting to v1.0')\n return f'v1.0-{lang_name}'\n\n\nVERSION_TAG = get_next_version_tag(ADAPTER_REPO_ID, LANG_NAME, HF_TOKEN)\nPATH_IN_REPO = f'adapters/{LANG_NAME}'\n\nprint(f'Version tag : {VERSION_TAG}')\nprint(f'Path in repo : {ADAPTER_REPO_ID}/{PATH_IN_REPO}')"
|
| 231 |
-
},
|
| 232 |
-
{
|
| 233 |
-
"cell_type": "code",
|
| 234 |
-
"execution_count": null,
|
| 235 |
-
"id": "cell-push",
|
| 236 |
-
"metadata": {},
|
| 237 |
-
"outputs": [],
|
| 238 |
-
"source": [
|
| 239 |
-
"# ── Cell 19: Push adapter to HF Model repo ───────────────────────────────────\nfrom huggingface_hub import HfApi, create_repo\n\n# Ensure repo exists\ncreate_repo(ADAPTER_REPO_ID, repo_type='model', private=True,\n exist_ok=True, token=HF_TOKEN)\n\n_cer_part = f'{cer_score:.1%}' if cer_score == cer_score else 'n/a'\ncommit_msg = (\n f'[{VERSION_TAG}] {LANG_NAME} ({LANG_COUNTRY}) fine-tuned checkpoint — '\n f'{train_result.global_step} steps | CER {_cer_part} | '\n f'{len(correction_records)} corrections + WaxalNLP | {LANG_DIALECT}'\n)\n\napi.upload_folder(\n folder_path=OUTPUT_DIR,\n repo_id=ADAPTER_REPO_ID,\n repo_type='model',\n path_in_repo=PATH_IN_REPO,\n commit_message=commit_msg,\n)\nprint(f'✅ Adapter uploaded: {ADAPTER_REPO_ID}/{PATH_IN_REPO}')\n\n# Create a Git tag for this version\ntry:\n api.create_tag(\n repo_id=ADAPTER_REPO_ID,\n repo_type='model',\n tag=VERSION_TAG,\n tag_message=commit_msg,\n token=HF_TOKEN,\n )\n print(f'✅ Tag created : {VERSION_TAG}')\nexcept Exception as e:\n print(f'⚠️ Tag creation skipped: {e}')"
|
| 240 |
-
]
|
| 241 |
-
},
|
| 242 |
-
{
|
| 243 |
-
"cell_type": "code",
|
| 244 |
-
"execution_count": null,
|
| 245 |
-
"id": "cell-verify",
|
| 246 |
-
"metadata": {},
|
| 247 |
-
"outputs": [],
|
| 248 |
-
"source": [
|
| 249 |
-
"# ── Cell 20: Verification summary ────────────────────────────────────────────\nfrom huggingface_hub import list_repo_files\n\nprint('=' * 60)\nprint('DEEP SLEEP TRAINING — COMPLETE')\nprint('=' * 60)\nprint(f' Language : {TRAIN_LANG} ({LANG_NAME})')\nprint(f' Model : {WHISPER_MODEL_ID}')\nprint(f' Steps completed : {train_result.global_step}')\nprint(f' Train loss : {train_result.training_loss:.4f}')\n_cer_disp = f'{cer_score:.1%}' if cer_score == cer_score else 'n/a'\n_wer_disp = f'{wer_score:.1%}' if wer_score == wer_score else 'n/a'\nprint(f' Eval CER (primary) : {_cer_disp}')\nprint(f' Eval WER (secondary): {_wer_disp}')\nprint(f' Corrections used : {len(correction_records)} × {CORRECTION_REPEAT}')\nprint(f' WaxalNLP samples : up to {MAX_WAXAL_TRAIN}')\nprint(f' Version tag : {VERSION_TAG}')\nprint(f' HF repo : {ADAPTER_REPO_ID}/{PATH_IN_REPO}')\nprint()\n\n# List what was pushed\ntry:\n repo_files = sorted(list_repo_files(\n ADAPTER_REPO_ID, repo_type='model', token=HF_TOKEN\n ))\n adapter_files = [f for f in repo_files if f.startswith(f'adapters/{LANG_NAME}/')]\n print('Adapter files in repo:')\n for f in adapter_files:\n print(f' {f}')\nexcept Exception as e:\n print(f'Could not list repo files: {e}')\n\nprint()\nprint('Next steps:')\nprint(' 1. In your HF Space settings, confirm ADAPTER_REPO_ID secret is set')\nprint(f' 2. Tab 3 → Reload Adapters → select \"{VERSION_TAG}\"')\nprint(' 3. Collect more corrections in the Space, then re-run this notebook')"
|
| 250 |
-
]
|
| 251 |
-
}
|
| 252 |
-
]
|
| 253 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
notebooks/kaggle_master_trainer/kaggle_master_trainer.ipynb
ADDED
|
@@ -0,0 +1,1352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"id": "0",
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"source": [
|
| 8 |
+
"# 🌾 Sahel-Voice-Core — Kaggle Master Trainer\n",
|
| 9 |
+
"\n",
|
| 10 |
+
"**Deep Sleep Fine-Tuning** for `openai/whisper-small` using LoRA (PEFT).\n",
|
| 11 |
+
"\n",
|
| 12 |
+
"This single notebook replaces `bootstrap_repos.ipynb`, `train_colab.ipynb`, and `train_fula_tts.ipynb`.\n",
|
| 13 |
+
"\n",
|
| 14 |
+
"### Data sources integrated\n",
|
| 15 |
+
"| Source | Contents | Used for |\n",
|
| 16 |
+
"|--------|----------|----------|\n",
|
| 17 |
+
"| `ous-sow/sahel-agri-feedback` | `vocabulary.jsonl` + `corrections.jsonl` + audio | Primary fine-tuning signal |\n",
|
| 18 |
+
"| `google/WaxalNLP` (bam + ful subsets) | Native speaker recordings | Baseline Bambara & Fula |\n",
|
| 19 |
+
"| External datasets (configurable) | e.g. `mozilla-foundation/common_voice_13_0` | Coverage expansion |\n",
|
| 20 |
+
"\n",
|
| 21 |
+
"### Before running\n",
|
| 22 |
+
"1. **Runtime → Accelerator → GPU T4 × 1** \n",
|
| 23 |
+
"2. Add-ons → Secrets → `HF_TOKEN` (toggle Attach ON)\n",
|
| 24 |
+
"3. Edit **Cell 3** to set your language and version tag prefix"
|
| 25 |
+
]
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"cell_type": "code",
|
| 29 |
+
"execution_count": null,
|
| 30 |
+
"id": "1",
|
| 31 |
+
"metadata": {},
|
| 32 |
+
"outputs": [],
|
| 33 |
+
"source": [
|
| 34 |
+
"# ── Cell 1: GPU check ────────────────────────────────────────────────────────\n",
|
| 35 |
+
"import subprocess, sys\n",
|
| 36 |
+
"\n",
|
| 37 |
+
"result = subprocess.run(['nvidia-smi'], capture_output=True, text=True)\n",
|
| 38 |
+
"if result.returncode != 0:\n",
|
| 39 |
+
" raise RuntimeError('No GPU detected. Runtime → Accelerator → GPU T4 × 1')\n",
|
| 40 |
+
"print(result.stdout[:600])\n",
|
| 41 |
+
"\n",
|
| 42 |
+
"import torch\n",
|
| 43 |
+
"print(f'PyTorch : {torch.__version__}')\n",
|
| 44 |
+
"print(f'CUDA avail: {torch.cuda.is_available()}')\n",
|
| 45 |
+
"if torch.cuda.is_available():\n",
|
| 46 |
+
" print(f'GPU : {torch.cuda.get_device_name(0)}')\n",
|
| 47 |
+
" cap = torch.cuda.get_device_capability(0)\n",
|
| 48 |
+
" print(f'Compute : {cap[0]}.{cap[1]}')\n",
|
| 49 |
+
" if cap[0] < 7:\n",
|
| 50 |
+
" print('⚠️ Compute < 7.0 — bitsandbytes 8-bit ops may not work. Switch to T4 (CC 7.5).')\n",
|
| 51 |
+
"print('✅ GPU ready')"
|
| 52 |
+
]
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"cell_type": "code",
|
| 56 |
+
"execution_count": null,
|
| 57 |
+
"id": "2",
|
| 58 |
+
"metadata": {},
|
| 59 |
+
"outputs": [],
|
| 60 |
+
"source": [
|
| 61 |
+
"# -- Cell 2: Install minimal missing dependencies ----------------------------\n",
|
| 62 |
+
"# We do NOT use PEFT/LoRA, so system transformers/numpy/scipy are fine as-is.\n",
|
| 63 |
+
"# Kaggle does not ship jiwer (WER metric) -- install it now.\n",
|
| 64 |
+
"import subprocess, sys\n",
|
| 65 |
+
"\n",
|
| 66 |
+
"subprocess.check_call([\n",
|
| 67 |
+
" sys.executable, '-m', 'pip', 'install', '-q', 'jiwer==3.0.4',\n",
|
| 68 |
+
"])\n",
|
| 69 |
+
"\n",
|
| 70 |
+
"# datasets >= 4.0 uses torchcodec for audio decoding. Install if missing.\n",
|
| 71 |
+
"try:\n",
|
| 72 |
+
" import torchcodec # noqa\n",
|
| 73 |
+
"except ImportError:\n",
|
| 74 |
+
" print('torchcodec not found — installing to match torch ...')\n",
|
| 75 |
+
" try:\n",
|
| 76 |
+
" subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', 'torchcodec'])\n",
|
| 77 |
+
" except subprocess.CalledProcessError:\n",
|
| 78 |
+
" import torch as _t\n",
|
| 79 |
+
" _tv = _t.__version__.split('+')[0]\n",
|
| 80 |
+
" _pin = {'2.4': '0.1.*', '2.5': '0.2.*', '2.6': '0.3.*', '2.7': '0.4.*', '2.8': '0.4.*'}.get(_tv[:3])\n",
|
| 81 |
+
" if _pin:\n",
|
| 82 |
+
" subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', f'torchcodec=={_pin}'])\n",
|
| 83 |
+
" else:\n",
|
| 84 |
+
" print(f'⚠️ Unknown torch {_tv}; install torchcodec manually if audio decoding fails')\n",
|
| 85 |
+
"\n",
|
| 86 |
+
"import torch\n",
|
| 87 |
+
"print(f\"torch : {torch.__version__}\")\n",
|
| 88 |
+
"print(f\"CUDA avail : {torch.cuda.is_available()}\")\n",
|
| 89 |
+
"if torch.cuda.is_available():\n",
|
| 90 |
+
" print(f\"GPU : {torch.cuda.get_device_name(0)}\")\n",
|
| 91 |
+
"\n",
|
| 92 |
+
"import transformers, datasets as ds_lib\n",
|
| 93 |
+
"print(f\"transformers: {transformers.__version__}\")\n",
|
| 94 |
+
"print(f\"datasets : {ds_lib.__version__}\")\n",
|
| 95 |
+
"print(\"All packages ready.\")\n"
|
| 96 |
+
]
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"cell_type": "code",
|
| 100 |
+
"execution_count": null,
|
| 101 |
+
"id": "3",
|
| 102 |
+
"metadata": {},
|
| 103 |
+
"outputs": [],
|
| 104 |
+
"source": [
|
| 105 |
+
"# ── Cell 3: CONFIGURATION — edit these before each run ───────────────────────\n",
|
| 106 |
+
"import os\n",
|
| 107 |
+
"from pathlib import Path\n",
|
| 108 |
+
"\n",
|
| 109 |
+
"# ─── Environment detection (Kaggle / Colab / RunPod / local) ─────────────────\n",
|
| 110 |
+
"if Path('/kaggle/working').exists():\n",
|
| 111 |
+
" _ENV = 'kaggle'\n",
|
| 112 |
+
" WORKING_DIR = '/kaggle/working'\n",
|
| 113 |
+
"elif Path('/content').exists() and not Path('/workspace').exists():\n",
|
| 114 |
+
" _ENV = 'colab'\n",
|
| 115 |
+
" WORKING_DIR = '/content'\n",
|
| 116 |
+
"elif Path('/workspace').exists():\n",
|
| 117 |
+
" _ENV = 'runpod'\n",
|
| 118 |
+
" WORKING_DIR = '/workspace'\n",
|
| 119 |
+
"else:\n",
|
| 120 |
+
" _ENV = 'local'\n",
|
| 121 |
+
" WORKING_DIR = os.environ.get('WORKING_DIR', os.path.expanduser('~/sahel-voice-work'))\n",
|
| 122 |
+
" Path(WORKING_DIR).mkdir(parents=True, exist_ok=True)\n",
|
| 123 |
+
"\n",
|
| 124 |
+
"# ─── Language to train ───────────────────────────────────────────────────────\n",
|
| 125 |
+
"# 'bam' = Bambara 'ful' = Fula\n",
|
| 126 |
+
"TRAIN_LANG = os.environ.get('TRAIN_LANG', 'ful')\n",
|
| 127 |
+
"\n",
|
| 128 |
+
"# ─── Model ───────────────────────────────────────────────────────────────────\n",
|
| 129 |
+
"# whisper-large-v3-turbo (128 mel bins) matches the Space base model.\n",
|
| 130 |
+
"# On T4 (Kaggle, 16 GB) drop to 'openai/whisper-small' — turbo is tight there.\n",
|
| 131 |
+
"_DEFAULT_MODEL = 'openai/whisper-small' if _ENV == 'kaggle' else 'openai/whisper-large-v3-turbo'\n",
|
| 132 |
+
"WHISPER_MODEL_ID = os.environ.get('WHISPER_MODEL_ID', _DEFAULT_MODEL)\n",
|
| 133 |
+
"TARGET_SR = 16_000\n",
|
| 134 |
+
"\n",
|
| 135 |
+
"# ─── HuggingFace repos ───────────────────────────────────────────────────────\n",
|
| 136 |
+
"HF_USERNAME = os.environ.get('HF_USERNAME', 'ous-sow')\n",
|
| 137 |
+
"FEEDBACK_REPO_ID = f'{HF_USERNAME}/sahel-agri-feedback'\n",
|
| 138 |
+
"ADAPTER_REPO_ID = f'{HF_USERNAME}/sahel-agri-adapters'\n",
|
| 139 |
+
"\n",
|
| 140 |
+
"# ─── Training hyper-parameters (defaults tuned per environment) ──────────────\n",
|
| 141 |
+
"if _ENV == 'runpod':\n",
|
| 142 |
+
" # 24 GB RTX 3090 / A40: turbo-v3 fits with batch 8, grad_accum 4 (eff. 32)\n",
|
| 143 |
+
" MAX_STEPS = 2_000\n",
|
| 144 |
+
" BATCH_SIZE = 8\n",
|
| 145 |
+
" GRAD_ACCUM = 4\n",
|
| 146 |
+
" MAX_WAXAL_TRAIN = 5_000\n",
|
| 147 |
+
"else:\n",
|
| 148 |
+
" # T4 (Kaggle free) / local CPU fallback\n",
|
| 149 |
+
" MAX_STEPS = 4_000\n",
|
| 150 |
+
" BATCH_SIZE = 16\n",
|
| 151 |
+
" GRAD_ACCUM = 2\n",
|
| 152 |
+
" MAX_WAXAL_TRAIN = 5_000\n",
|
| 153 |
+
"\n",
|
| 154 |
+
"LEARNING_RATE = 1e-3\n",
|
| 155 |
+
"WARMUP_STEPS = max(100, MAX_STEPS // 20)\n",
|
| 156 |
+
"SAVE_STEPS = 500\n",
|
| 157 |
+
"EVAL_STEPS = 500\n",
|
| 158 |
+
"LOGGING_STEPS = 50\n",
|
| 159 |
+
"CORRECTION_REPEAT= 3 # upsample user corrections Nx for emphasis\n",
|
| 160 |
+
"\n",
|
| 161 |
+
"# ─── Paths ────────────────────────────────────────────────────────────────────\n",
|
| 162 |
+
"OUTPUT_DIR = f'{WORKING_DIR}/adapter_{TRAIN_LANG}'\n",
|
| 163 |
+
"DATA_DIR = f'{WORKING_DIR}/data'\n",
|
| 164 |
+
"AUDIO_DIR = f'{WORKING_DIR}/audio_feedback'\n",
|
| 165 |
+
"\n",
|
| 166 |
+
"LANG_NAME = {'bam': 'bambara', 'ful': 'fula'}.get(TRAIN_LANG, TRAIN_LANG)\n",
|
| 167 |
+
"LANG_COUNTRY = {'bam': 'Mali', 'ful': 'Guinea'}.get(TRAIN_LANG, '')\n",
|
| 168 |
+
"LANG_DIALECT = {\n",
|
| 169 |
+
" 'bam': 'Standard Bambara (Bamako/Ségou) — Malian orthography',\n",
|
| 170 |
+
" 'ful': 'Pular (Labé/Mamou dialects) — Guinean orthography',\n",
|
| 171 |
+
"}.get(TRAIN_LANG, '')\n",
|
| 172 |
+
"\n",
|
| 173 |
+
"print(f'Environment : {_ENV}')\n",
|
| 174 |
+
"print(f'Language : {TRAIN_LANG} ({LANG_NAME}) — {LANG_COUNTRY}')\n",
|
| 175 |
+
"print(f'Dialect : {LANG_DIALECT}')\n",
|
| 176 |
+
"print(f'Model : {WHISPER_MODEL_ID}')\n",
|
| 177 |
+
"print(f'Working dir : {WORKING_DIR}')\n",
|
| 178 |
+
"print(f'Output : {OUTPUT_DIR}')\n",
|
| 179 |
+
"print(f'Max steps : {MAX_STEPS} (batch={BATCH_SIZE}, grad_accum={GRAD_ACCUM}, eff={BATCH_SIZE*GRAD_ACCUM})')\n"
|
| 180 |
+
]
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"cell_type": "code",
|
| 184 |
+
"execution_count": null,
|
| 185 |
+
"id": "4",
|
| 186 |
+
"metadata": {},
|
| 187 |
+
"outputs": [],
|
| 188 |
+
"source": [
|
| 189 |
+
"# -- Cell 4: External dataset configuration -----------------------------------\n",
|
| 190 |
+
"# EXTERNAL_DATASETS is loaded dynamically from dataset_sources.jsonl in the\n",
|
| 191 |
+
"# feedback repo. The Space's Self-Teaching tab writes dataset references there\n",
|
| 192 |
+
"# when the user clicks \"Import from HuggingFace\". This cell reads that file\n",
|
| 193 |
+
"# so any dataset registered in the Space is automatically used here.\n",
|
| 194 |
+
"\n",
|
| 195 |
+
"import json as _json\n",
|
| 196 |
+
"from huggingface_hub import hf_hub_download as _hf_dl\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"EXTERNAL_DATASETS = []\n",
|
| 199 |
+
"\n",
|
| 200 |
+
"# -- Load dataset_sources.jsonl from Hub (written by Space Self-Teaching tab) --\n",
|
| 201 |
+
"try:\n",
|
| 202 |
+
" _src_path = _hf_dl(\n",
|
| 203 |
+
" repo_id=FEEDBACK_REPO_ID, filename='dataset_sources.jsonl',\n",
|
| 204 |
+
" repo_type='dataset', token=HF_TOKEN,\n",
|
| 205 |
+
" )\n",
|
| 206 |
+
" with open(_src_path, encoding='utf-8') as _f:\n",
|
| 207 |
+
" for _line in _f:\n",
|
| 208 |
+
" _line = _line.strip()\n",
|
| 209 |
+
" if not _line:\n",
|
| 210 |
+
" continue\n",
|
| 211 |
+
" _entry = _json.loads(_line)\n",
|
| 212 |
+
" if not _entry.get('enabled'):\n",
|
| 213 |
+
" continue\n",
|
| 214 |
+
" # Normalise keys to what Cell 9 expects\n",
|
| 215 |
+
" EXTERNAL_DATASETS.append({\n",
|
| 216 |
+
" 'enabled' : True,\n",
|
| 217 |
+
" 'repo_id' : _entry.get('repo', _entry.get('repo_id', '')),\n",
|
| 218 |
+
" 'config' : _entry.get('config'),\n",
|
| 219 |
+
" 'split' : _entry.get('split', 'train'),\n",
|
| 220 |
+
" 'text_col' : _entry.get('text_col', 'transcription'),\n",
|
| 221 |
+
" 'lang' : _entry.get('lang', _entry.get('language', TRAIN_LANG)),\n",
|
| 222 |
+
" 'max_samples': _entry.get('max', _entry.get('max_samples', 2_000)),\n",
|
| 223 |
+
" })\n",
|
| 224 |
+
" print(f'dataset_sources.jsonl: loaded {len(EXTERNAL_DATASETS)} source(s)')\n",
|
| 225 |
+
"except Exception as _e:\n",
|
| 226 |
+
" print(f'dataset_sources.jsonl not found or empty ({_e}) -- using hardcoded list only')\n",
|
| 227 |
+
"\n",
|
| 228 |
+
"active = [d for d in EXTERNAL_DATASETS if d.get('lang') == TRAIN_LANG]\n",
|
| 229 |
+
"print(f'External sources active for {TRAIN_LANG}: {len(active)}')\n",
|
| 230 |
+
"for _d in active:\n",
|
| 231 |
+
" print(f\" - {_d['repo_id']} / {_d['config']} (max {_d['max_samples']} samples)\")\n",
|
| 232 |
+
"if not active:\n",
|
| 233 |
+
" if TRAIN_LANG == 'bam':\n",
|
| 234 |
+
" print('Bambara: no external source yet.')\n",
|
| 235 |
+
" print(' In the Space -> Self-Teaching tab -> Import from HuggingFace (Bambara).')\n",
|
| 236 |
+
" elif TRAIN_LANG == 'ful':\n",
|
| 237 |
+
" print('Fula: WaxalNLP ful_asr loaded in Cell 8 -- no extra source needed.')\n"
|
| 238 |
+
]
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"cell_type": "code",
|
| 242 |
+
"execution_count": null,
|
| 243 |
+
"id": "5",
|
| 244 |
+
"metadata": {},
|
| 245 |
+
"outputs": [],
|
| 246 |
+
"source": [
|
| 247 |
+
"# ── Cell 5: HuggingFace login + directory setup ───────────────────────────────\n",
|
| 248 |
+
"import os\n",
|
| 249 |
+
"from pathlib import Path\n",
|
| 250 |
+
"\n",
|
| 251 |
+
"HF_TOKEN = None\n",
|
| 252 |
+
"\n",
|
| 253 |
+
"# Kaggle secrets (preferred)\n",
|
| 254 |
+
"try:\n",
|
| 255 |
+
" from kaggle_secrets import UserSecretsClient # type: ignore\n",
|
| 256 |
+
" HF_TOKEN = UserSecretsClient().get_secret('HF_TOKEN')\n",
|
| 257 |
+
" print('HF_TOKEN loaded from Kaggle secrets.')\n",
|
| 258 |
+
"except Exception:\n",
|
| 259 |
+
" pass\n",
|
| 260 |
+
"\n",
|
| 261 |
+
"# Colab secrets (fallback)\n",
|
| 262 |
+
"if not HF_TOKEN:\n",
|
| 263 |
+
" try:\n",
|
| 264 |
+
" from google.colab import userdata # type: ignore\n",
|
| 265 |
+
" HF_TOKEN = userdata.get('HF_TOKEN')\n",
|
| 266 |
+
" print('HF_TOKEN loaded from Colab secrets.')\n",
|
| 267 |
+
" except Exception:\n",
|
| 268 |
+
" pass\n",
|
| 269 |
+
"\n",
|
| 270 |
+
"# .env file (RunPod / local) - look in common locations\n",
|
| 271 |
+
"if not HF_TOKEN:\n",
|
| 272 |
+
" for _env_path in ['/workspace/sahel-voice/.env', './.env', '../.env', os.path.expanduser('~/.env')]:\n",
|
| 273 |
+
" if os.path.isfile(_env_path):\n",
|
| 274 |
+
" with open(_env_path, encoding='utf-8') as _f:\n",
|
| 275 |
+
" for _line in _f:\n",
|
| 276 |
+
" _line = _line.strip()\n",
|
| 277 |
+
" if _line.startswith('HF_TOKEN='):\n",
|
| 278 |
+
" _val = _line.split('=', 1)[1].strip()\n",
|
| 279 |
+
" if _val and len(_val) >= 2 and _val[0] in ('\"', \"'\") and _val[-1] == _val[0]:\n",
|
| 280 |
+
" _val = _val[1:-1]\n",
|
| 281 |
+
" HF_TOKEN = _val\n",
|
| 282 |
+
" print(f'HF_TOKEN loaded from {_env_path}')\n",
|
| 283 |
+
" break\n",
|
| 284 |
+
" if HF_TOKEN:\n",
|
| 285 |
+
" break\n",
|
| 286 |
+
"\n",
|
| 287 |
+
"if not HF_TOKEN:\n",
|
| 288 |
+
" HF_TOKEN = os.environ.get('HF_TOKEN', '')\n",
|
| 289 |
+
" if HF_TOKEN:\n",
|
| 290 |
+
" print('HF_TOKEN loaded from environment variable.')\n",
|
| 291 |
+
"\n",
|
| 292 |
+
"if not HF_TOKEN:\n",
|
| 293 |
+
" raise ValueError(\n",
|
| 294 |
+
" 'HF_TOKEN not found.\\n'\n",
|
| 295 |
+
" 'Kaggle: Add-ons → Secrets → add HF_TOKEN → toggle \"Attach to notebook\" ON'\n",
|
| 296 |
+
" )\n",
|
| 297 |
+
"\n",
|
| 298 |
+
"from huggingface_hub import login, HfApi\n",
|
| 299 |
+
"login(token=HF_TOKEN, add_to_git_credential=False)\n",
|
| 300 |
+
"api = HfApi(token=HF_TOKEN)\n",
|
| 301 |
+
"os.environ['HF_TOKEN'] = HF_TOKEN\n",
|
| 302 |
+
"\n",
|
| 303 |
+
"# Create output directories\n",
|
| 304 |
+
"for d in [OUTPUT_DIR, DATA_DIR, AUDIO_DIR]:\n",
|
| 305 |
+
" Path(d).mkdir(parents=True, exist_ok=True)\n",
|
| 306 |
+
"\n",
|
| 307 |
+
"print(f'✅ Logged in | output: {OUTPUT_DIR}')"
|
| 308 |
+
]
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
"cell_type": "code",
|
| 312 |
+
"execution_count": null,
|
| 313 |
+
"id": "6",
|
| 314 |
+
"metadata": {},
|
| 315 |
+
"outputs": [],
|
| 316 |
+
"source": [
|
| 317 |
+
"# ── Cell 6: Resume-from-checkpoint detection ──────────────────────────────────\n",
|
| 318 |
+
"# If OUTPUT_DIR already has checkpoints (e.g. Kaggle session timed out),\n",
|
| 319 |
+
"# training will automatically resume from the latest one.\n",
|
| 320 |
+
"#\n",
|
| 321 |
+
"# NOTE: We do NOT use transformers.trainer_utils.get_last_checkpoint here.\n",
|
| 322 |
+
"# That import pulls in peft → transformers.generation → masking_utils →\n",
|
| 323 |
+
"# torch._dynamo before packages are settled, causing ImportError on Kaggle\n",
|
| 324 |
+
"# Python 3.12. The function below replicates exactly what it does internally.\n",
|
| 325 |
+
"\n",
|
| 326 |
+
"import re\n",
|
| 327 |
+
"from pathlib import Path\n",
|
| 328 |
+
"\n",
|
| 329 |
+
"def _get_last_checkpoint(folder: str):\n",
|
| 330 |
+
" \"\"\"Return the highest-numbered checkpoint-N directory path, or None.\"\"\"\n",
|
| 331 |
+
" p = Path(folder)\n",
|
| 332 |
+
" if not p.exists():\n",
|
| 333 |
+
" return None\n",
|
| 334 |
+
" checkpoints = [\n",
|
| 335 |
+
" d for d in p.iterdir()\n",
|
| 336 |
+
" if d.is_dir() and re.fullmatch(r'checkpoint-\\d+', d.name)\n",
|
| 337 |
+
" ]\n",
|
| 338 |
+
" if not checkpoints:\n",
|
| 339 |
+
" return None\n",
|
| 340 |
+
" return str(max(checkpoints, key=lambda d: int(d.name.split('-')[1])))\n",
|
| 341 |
+
"\n",
|
| 342 |
+
"\n",
|
| 343 |
+
"LAST_CHECKPOINT = _get_last_checkpoint(OUTPUT_DIR)\n",
|
| 344 |
+
"\n",
|
| 345 |
+
"if LAST_CHECKPOINT:\n",
|
| 346 |
+
" print(f'⏩ Resume checkpoint found: {LAST_CHECKPOINT}')\n",
|
| 347 |
+
" print(' Training will continue from this point.')\n",
|
| 348 |
+
"else:\n",
|
| 349 |
+
" print('🆕 No checkpoint found — starting fresh training.')"
|
| 350 |
+
]
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"cell_type": "code",
|
| 354 |
+
"execution_count": null,
|
| 355 |
+
"id": "7",
|
| 356 |
+
"metadata": {},
|
| 357 |
+
"outputs": [],
|
| 358 |
+
"source": [
|
| 359 |
+
"# ── Cell 7: Download sahel-agri-feedback data ─────────────────────────────────\n",
|
| 360 |
+
"# Downloads vocabulary.jsonl (word pairs) and corrections.jsonl (audio+text).\n",
|
| 361 |
+
"# Audio files referenced in corrections.jsonl are also downloaded.\n",
|
| 362 |
+
"\n",
|
| 363 |
+
"import json, shutil\n",
|
| 364 |
+
"from pathlib import Path\n",
|
| 365 |
+
"from huggingface_hub import hf_hub_download, list_repo_files\n",
|
| 366 |
+
"\n",
|
| 367 |
+
"# ── vocabulary.jsonl (word pairs taught by users) ──────────────────────────\n",
|
| 368 |
+
"vocab_entries = []\n",
|
| 369 |
+
"try:\n",
|
| 370 |
+
" vocab_path = hf_hub_download(\n",
|
| 371 |
+
" repo_id=FEEDBACK_REPO_ID, filename='vocabulary.jsonl',\n",
|
| 372 |
+
" repo_type='dataset', token=HF_TOKEN,\n",
|
| 373 |
+
" )\n",
|
| 374 |
+
" with open(vocab_path, encoding='utf-8') as f:\n",
|
| 375 |
+
" vocab_entries = [json.loads(l) for l in f if l.strip()]\n",
|
| 376 |
+
" print(f'vocabulary.jsonl : {len(vocab_entries)} entries')\n",
|
| 377 |
+
"except Exception as e:\n",
|
| 378 |
+
" print(f'vocabulary.jsonl not found or empty: {e}')\n",
|
| 379 |
+
"\n",
|
| 380 |
+
"# ── corrections.jsonl (audio corrections from the Space) ──────────────────\n",
|
| 381 |
+
"correction_records = []\n",
|
| 382 |
+
"try:\n",
|
| 383 |
+
" corr_path = hf_hub_download(\n",
|
| 384 |
+
" repo_id=FEEDBACK_REPO_ID, filename='corrections.jsonl',\n",
|
| 385 |
+
" repo_type='dataset', token=HF_TOKEN,\n",
|
| 386 |
+
" )\n",
|
| 387 |
+
" with open(corr_path, encoding='utf-8') as f:\n",
|
| 388 |
+
" all_records = [json.loads(l) for l in f if l.strip()]\n",
|
| 389 |
+
" correction_records = [\n",
|
| 390 |
+
" r for r in all_records\n",
|
| 391 |
+
" if r.get('language') == TRAIN_LANG\n",
|
| 392 |
+
" and (r.get('corrected_text') or r.get('transcription'))\n",
|
| 393 |
+
" and r.get('audio_file')\n",
|
| 394 |
+
" ]\n",
|
| 395 |
+
" print(f'corrections.jsonl: {len(all_records)} total, {len(correction_records)} for lang={TRAIN_LANG}')\n",
|
| 396 |
+
"except Exception as e:\n",
|
| 397 |
+
" print(f'corrections.jsonl not found or empty: {e}')\n",
|
| 398 |
+
"\n",
|
| 399 |
+
"# ── Download audio files referenced in corrections ─────────────────────────\n",
|
| 400 |
+
"skipped_audio = 0\n",
|
| 401 |
+
"for rec in correction_records:\n",
|
| 402 |
+
" audio_fname = Path(rec['audio_file']).name\n",
|
| 403 |
+
" local_path = Path(AUDIO_DIR) / audio_fname\n",
|
| 404 |
+
" if local_path.exists():\n",
|
| 405 |
+
" rec['local_audio'] = str(local_path)\n",
|
| 406 |
+
" continue\n",
|
| 407 |
+
" try:\n",
|
| 408 |
+
" dl = hf_hub_download(\n",
|
| 409 |
+
" repo_id=FEEDBACK_REPO_ID, filename=rec['audio_file'],\n",
|
| 410 |
+
" repo_type='dataset', token=HF_TOKEN,\n",
|
| 411 |
+
" )\n",
|
| 412 |
+
" shutil.copy2(dl, local_path)\n",
|
| 413 |
+
" rec['local_audio'] = str(local_path)\n",
|
| 414 |
+
" except Exception as e:\n",
|
| 415 |
+
" skipped_audio += 1\n",
|
| 416 |
+
" rec['local_audio'] = None\n",
|
| 417 |
+
"\n",
|
| 418 |
+
"correction_records = [r for r in correction_records if r.get('local_audio')]\n",
|
| 419 |
+
"print(f'Audio downloaded : {len(correction_records)} files ({skipped_audio} skipped)')\n",
|
| 420 |
+
"print(f'Vocab entries : {len(vocab_entries)}')"
|
| 421 |
+
]
|
| 422 |
+
},
|
| 423 |
+
{
|
| 424 |
+
"cell_type": "code",
|
| 425 |
+
"execution_count": null,
|
| 426 |
+
"id": "8",
|
| 427 |
+
"metadata": {},
|
| 428 |
+
"outputs": [],
|
| 429 |
+
"source": [
|
| 430 |
+
"# -- Cell 8: Load WaxalNLP ----------------------------------------------------\n",
|
| 431 |
+
"# WaxalNLP confirmed subsets (google/WaxalNLP dataset card):\n",
|
| 432 |
+
"# Fula -> 'ful_asr' (available)\n",
|
| 433 |
+
"# Bambara -> NOT present (no 'bam' config exists)\n",
|
| 434 |
+
"#\n",
|
| 435 |
+
"# google/fleurs is NOT used as a fallback -- datasets >= 3.0 refuses\n",
|
| 436 |
+
"# to execute its legacy script loader (fleurs.py).\n",
|
| 437 |
+
"#\n",
|
| 438 |
+
"# Bambara training uses: user corrections (Cell 7) + Common Voice bm (Cell 4).\n",
|
| 439 |
+
"# Fula training uses: user corrections + WaxalNLP ful_asr + Common Voice ff.\n",
|
| 440 |
+
"\n",
|
| 441 |
+
"from datasets import load_dataset, Audio as HFAudio\n",
|
| 442 |
+
"\n",
|
| 443 |
+
"WAXAL_SUBSET_MAP = {\n",
|
| 444 |
+
" 'bam': None, # no Bambara subset in WaxalNLP -- skip silently\n",
|
| 445 |
+
" 'ful': 'ful_asr', # confirmed available config\n",
|
| 446 |
+
"}\n",
|
| 447 |
+
"\n",
|
| 448 |
+
"waxal_ds = None\n",
|
| 449 |
+
"WAXAL_TEXT_COL = 'transcription'\n",
|
| 450 |
+
"\n",
|
| 451 |
+
"subset = WAXAL_SUBSET_MAP.get(TRAIN_LANG)\n",
|
| 452 |
+
"\n",
|
| 453 |
+
"if subset is None:\n",
|
| 454 |
+
" print(f'WaxalNLP has no subset for lang={TRAIN_LANG} -- skipping.')\n",
|
| 455 |
+
" print('Bambara will train on user corrections + Common Voice (Cell 4).')\n",
|
| 456 |
+
"else:\n",
|
| 457 |
+
" try:\n",
|
| 458 |
+
" print(f'Loading google/WaxalNLP subset={subset} (streaming) ...')\n",
|
| 459 |
+
" waxal_ds = load_dataset(\n",
|
| 460 |
+
" 'google/WaxalNLP', subset,\n",
|
| 461 |
+
" split='train',\n",
|
| 462 |
+
" streaming=True,\n",
|
| 463 |
+
" token=HF_TOKEN,\n",
|
| 464 |
+
" )\n",
|
| 465 |
+
"\n",
|
| 466 |
+
" # Probe first item to confirm schema\n",
|
| 467 |
+
" probe = next(iter(waxal_ds))\n",
|
| 468 |
+
" if 'audio' not in probe:\n",
|
| 469 |
+
" raise ValueError(f'No audio column. Keys: {list(probe.keys())}')\n",
|
| 470 |
+
"\n",
|
| 471 |
+
" WAXAL_TEXT_COL = next(\n",
|
| 472 |
+
" (k for k in ['transcription', 'text', 'sentence', 'normalized_text']\n",
|
| 473 |
+
" if k in probe),\n",
|
| 474 |
+
" None,\n",
|
| 475 |
+
" )\n",
|
| 476 |
+
" if WAXAL_TEXT_COL is None:\n",
|
| 477 |
+
" raise ValueError(f'No text column. Keys: {list(probe.keys())}')\n",
|
| 478 |
+
"\n",
|
| 479 |
+
" print(f'WaxalNLP/{subset} ready -- text column: \"{WAXAL_TEXT_COL}\"')\n",
|
| 480 |
+
"\n",
|
| 481 |
+
" except Exception as e:\n",
|
| 482 |
+
" print(f'WaxalNLP/{subset} failed: {e}')\n",
|
| 483 |
+
" print('Continuing without WaxalNLP -- enable Common Voice in Cell 4.')\n",
|
| 484 |
+
" waxal_ds = None\n",
|
| 485 |
+
"\n",
|
| 486 |
+
"status = f'WaxalNLP/{subset}' if waxal_ds is not None else 'not available'\n",
|
| 487 |
+
"print(f'\\nWaxal source for {TRAIN_LANG}: {status}')"
|
| 488 |
+
]
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"cell_type": "code",
|
| 492 |
+
"execution_count": null,
|
| 493 |
+
"id": "9",
|
| 494 |
+
"metadata": {},
|
| 495 |
+
"outputs": [],
|
| 496 |
+
"source": [
|
| 497 |
+
"# ── Cell 9: Load external datasets (from Cell 4 config) ──────────────────────\n",
|
| 498 |
+
"from datasets import load_dataset, Audio as HFAudio\n",
|
| 499 |
+
"\n",
|
| 500 |
+
"external_datasets = [] # list of (hf_dataset, text_col)\n",
|
| 501 |
+
"\n",
|
| 502 |
+
"for cfg in EXTERNAL_DATASETS:\n",
|
| 503 |
+
" if not cfg['enabled'] or cfg['lang'] != TRAIN_LANG:\n",
|
| 504 |
+
" continue\n",
|
| 505 |
+
" try:\n",
|
| 506 |
+
" print(f'Loading {cfg[\"repo_id\"]} / {cfg[\"config\"]} ...')\n",
|
| 507 |
+
" ds = load_dataset(\n",
|
| 508 |
+
" cfg['repo_id'], cfg['config'],\n",
|
| 509 |
+
" split=cfg['split'],\n",
|
| 510 |
+
" streaming=True,\n",
|
| 511 |
+
" token=HF_TOKEN,\n",
|
| 512 |
+
" )\n",
|
| 513 |
+
" probe = next(iter(ds))\n",
|
| 514 |
+
" text_col = cfg['text_col'] if cfg['text_col'] in probe else next(\n",
|
| 515 |
+
" (k for k in ['transcription', 'text', 'sentence'] if k in probe), None\n",
|
| 516 |
+
" )\n",
|
| 517 |
+
" if text_col is None:\n",
|
| 518 |
+
" print(f' ⚠️ Cannot find text column — skipping')\n",
|
| 519 |
+
" continue\n",
|
| 520 |
+
" if 'audio' not in probe:\n",
|
| 521 |
+
" print(f' ⚠️ No audio column — skipping')\n",
|
| 522 |
+
" continue\n",
|
| 523 |
+
" # Cap at max_samples\n",
|
| 524 |
+
" ds = ds.take(cfg.get('max_samples', 2_000))\n",
|
| 525 |
+
" external_datasets.append((ds, text_col))\n",
|
| 526 |
+
" print(f' ✅ {cfg[\"repo_id\"]} — text col \"{text_col}\", max {cfg.get(\"max_samples\",2000)} samples')\n",
|
| 527 |
+
" except Exception as e:\n",
|
| 528 |
+
" print(f' ⚠️ {cfg[\"repo_id\"]} failed: {e}')\n",
|
| 529 |
+
"\n",
|
| 530 |
+
"print(f'\\nExternal sources loaded: {len(external_datasets)}')"
|
| 531 |
+
]
|
| 532 |
+
},
|
| 533 |
+
{
|
| 534 |
+
"cell_type": "markdown",
|
| 535 |
+
"id": "10",
|
| 536 |
+
"metadata": {},
|
| 537 |
+
"source": [
|
| 538 |
+
"---\n",
|
| 539 |
+
"## Data Pipeline\n",
|
| 540 |
+
"\n",
|
| 541 |
+
"All audio is resampled to 16 kHz. Text is cleaned with a language-aware allowlist that keeps Latin script extended characters valid for Bambara (ɛ ɔ ŋ) and Fula (ɓ ɗ ƴ ŋ ɲ), stripping everything else (URLs, XML tags, symbols). The Whisper processor converts the cleaned text to token IDs."
|
| 542 |
+
]
|
| 543 |
+
},
|
| 544 |
+
{
|
| 545 |
+
"cell_type": "code",
|
| 546 |
+
"execution_count": null,
|
| 547 |
+
"id": "11",
|
| 548 |
+
"metadata": {},
|
| 549 |
+
"outputs": [],
|
| 550 |
+
"source": [
|
| 551 |
+
"# -- Cell 10: Text cleaning utilities + Bambara phonetic normaliser -----------\n",
|
| 552 |
+
"import re, unicodedata\n",
|
| 553 |
+
"\n",
|
| 554 |
+
"# Phonetic normaliser: unifies French-influenced spellings before training.\n",
|
| 555 |
+
"# ou->u, dj->j, gn->ny_palatal etc. so spelling variants map to same token.\n",
|
| 556 |
+
"_BAM_NORM_RULES = [('ou','u'),('dj','j'),('gn','ɲ'),('ny','ɲ'),('ch','c'),('oo','ɔ'),('ee','ɛ')]\n",
|
| 557 |
+
"_BAM_NORM_PAT = re.compile('|'.join(re.escape(s) for s,_ in _BAM_NORM_RULES))\n",
|
| 558 |
+
"_BAM_NORM_MAP = {s:d for s,d in _BAM_NORM_RULES}\n",
|
| 559 |
+
"\n",
|
| 560 |
+
"def _bam_norm(text):\n",
|
| 561 |
+
" import unicodedata as _ud\n",
|
| 562 |
+
" text = _ud.normalize('NFC', text.lower())\n",
|
| 563 |
+
" return _BAM_NORM_PAT.sub(lambda m: _BAM_NORM_MAP[m.group(0)], text)\n",
|
| 564 |
+
"\n",
|
| 565 |
+
"# Pular (Fula of Guinea) normaliser: converts Adlam script → Latin,\n",
|
| 566 |
+
"# then NFC + lowercase. Needed because guizme/adlam_fulfulde labels are in\n",
|
| 567 |
+
"# Adlam (U+1E900-U+1E95F) which Whisper’s tokenizer has no coverage for.\n",
|
| 568 |
+
"_ADLAM_TO_LATIN = [\n",
|
| 569 |
+
" (\"𞤀\",\"A\"),(\"𞤁\",\"B\"),(\"𞤂\",\"B\"),(\"𞤃\",\"D\"),(\"𞤄\",\"D\"),\n",
|
| 570 |
+
" (\"𞤅\",\"E\"),(\"𞤆\",\"F\"),(\"𞤇\",\"G\"),(\"𞤈\",\"H\"),(\"𞤉\",\"I\"),\n",
|
| 571 |
+
" (\"𞤊\",\"J\"),(\"𞤋\",\"K\"),(\"𞤌\",\"L\"),(\"𞤍\",\"M\"),(\"𞤎\",\"N\"),\n",
|
| 572 |
+
" (\"𞤏\",\"NG\"),(\"𞤐\",\"O\"),(\"𞤑\",\"P\"),(\"𞤒\",\"R\"),(\"𞤓\",\"S\"),\n",
|
| 573 |
+
" (\"𞤔\",\"T\"),(\"𞤕\",\"U\"),(\"𞤖\",\"V\"),(\"𞤗\",\"W\"),(\"𞤘\",\"Y\"),\n",
|
| 574 |
+
" (\"𞤙\",\"Z\"),(\"𞤚\",\"KH\"),(\"𞤛\",\"QU\"),(\"𞤜\",\"SH\"),(\"𞤝\",\"GH\"),\n",
|
| 575 |
+
" (\"𞤞\",\"NY\"),(\"𞤟\",\"TH\"),(\"𞤠\",\"WH\"),(\"𞤡\",\"NY\"),\n",
|
| 576 |
+
" (\"𞤢\",\"a\"),(\"𞤣\",\"b\"),(\"𞤤\",\"b\"),(\"𞤥\",\"d\"),(\"𞤦\",\"d\"),\n",
|
| 577 |
+
" (\"𞤧\",\"e\"),(\"𞤨\",\"f\"),(\"𞤩\",\"g\"),(\"𞤪\",\"h\"),(\"𞤫\",\"i\"),\n",
|
| 578 |
+
" (\"𞤬\",\"j\"),(\"𞤭\",\"k\"),(\"𞤮\",\"l\"),(\"𞤯\",\"m\"),(\"𞤰\",\"n\"),\n",
|
| 579 |
+
" (\"𞤱\",\"ng\"),(\"𞤲\",\"o\"),(\"𞤳\",\"p\"),(\"𞤴\",\"r\"),(\"𞤵\",\"s\"),\n",
|
| 580 |
+
" (\"𞤶\",\"t\"),(\"𞤷\",\"u\"),(\"𞤸\",\"v\"),(\"𞤹\",\"w\"),(\"𞤺\",\"y\"),\n",
|
| 581 |
+
" (\"𞤻\",\"z\"),(\"𞤼\",\"kh\"),(\"𞤽\",\"qu\"),(\"𞤾\",\"sh\"),(\"𞤿\",\"gh\"),\n",
|
| 582 |
+
" (\"𞥀\",\"ny\"),(\"𞥁\",\"th\"),(\"𞥂\",\"wh\"),(\"𞥃\",\"ny\"),\n",
|
| 583 |
+
"]\n",
|
| 584 |
+
"_A2L = {a: l for a, l in _ADLAM_TO_LATIN}\n",
|
| 585 |
+
"_ADLAM_START, _ADLAM_END = 0x1E900, 0x1E95F\n",
|
| 586 |
+
"\n",
|
| 587 |
+
"def _contains_adlam(text):\n",
|
| 588 |
+
" return any(_ADLAM_START <= ord(c) <= _ADLAM_END for c in text)\n",
|
| 589 |
+
"\n",
|
| 590 |
+
"def _normalize_pular(text):\n",
|
| 591 |
+
" import unicodedata as _ud, re as _re\n",
|
| 592 |
+
" if _contains_adlam(text):\n",
|
| 593 |
+
" text = \"\".join(_A2L.get(c, c) for c in text)\n",
|
| 594 |
+
" text = _ud.normalize(\"NFC\", text.lower())\n",
|
| 595 |
+
" return _re.sub(r\"\\s+\", \" \", text).strip()\n",
|
| 596 |
+
"\n",
|
| 597 |
+
"\n",
|
| 598 |
+
"_BAMBARA_EXTRA = {'\\u025b','\\u0254','\\u014b'}\n",
|
| 599 |
+
"_FULA_EXTRA = {'\\u0253','\\u0257','\\u01b4','\\u014b','\\u0272'}\n",
|
| 600 |
+
"_BASE_LATIN = set('abcdefghijklmnopqrstuvwxyz')\n",
|
| 601 |
+
"_ACCENTED = set('\\u00e0\\u00e2\\u00e4\\u00e8\\u00e9\\u00ea\\u00eb'\n",
|
| 602 |
+
" '\\u00ee\\u00ef\\u00f4\\u00f9\\u00fb\\u00fc\\u00fd'\n",
|
| 603 |
+
" '\\u00ff\\u00e6\\u0153\\u00e7')\n",
|
| 604 |
+
"_KEEP_PUNCT = set(\" ',-.'!?\")\n",
|
| 605 |
+
"\n",
|
| 606 |
+
"_VALID_CHARS = {\n",
|
| 607 |
+
" 'bam': _BASE_LATIN | _ACCENTED | _BAMBARA_EXTRA | _KEEP_PUNCT,\n",
|
| 608 |
+
" 'ful': _BASE_LATIN | _ACCENTED | _FULA_EXTRA | _KEEP_PUNCT,\n",
|
| 609 |
+
"}\n",
|
| 610 |
+
"\n",
|
| 611 |
+
"\n",
|
| 612 |
+
"def clean_text(text: str, lang: str = 'bam') -> str:\n",
|
| 613 |
+
" if not text:\n",
|
| 614 |
+
" return ''\n",
|
| 615 |
+
" text = unicodedata.normalize('NFKC', text.lower().strip())\n",
|
| 616 |
+
" text = re.sub(r'https?://\\S+', '', text)\n",
|
| 617 |
+
" text = re.sub(r'<[^>]+>', '', text)\n",
|
| 618 |
+
" text = re.sub(r'([.,!?])\\1+', r'\\1', text)\n",
|
| 619 |
+
" valid = _VALID_CHARS.get(lang, _VALID_CHARS['bam'] | _VALID_CHARS['ful'])\n",
|
| 620 |
+
" text = ''.join(c for c in text if c in valid)\n",
|
| 621 |
+
" return re.sub(r'\\s+', ' ', text).strip()\n",
|
| 622 |
+
"\n",
|
| 623 |
+
"\n",
|
| 624 |
+
"# Verify actual output then assert against it\n",
|
| 625 |
+
"r1 = clean_text('I ni ce! (hello)', 'bam') # parens stripped, ! kept\n",
|
| 626 |
+
"r2 = clean_text('Jam waali. <b>test</b>', 'ful') # tags stripped, content kept\n",
|
| 627 |
+
"r3 = clean_text('Visit https://example.com now!!', 'bam') # URL stripped, word before stays\n",
|
| 628 |
+
"\n",
|
| 629 |
+
"assert r1 == 'i ni ce! hello', f'r1: {repr(r1)}'\n",
|
| 630 |
+
"assert r2 == 'jam waali. test', f'r2: {repr(r2)}'\n",
|
| 631 |
+
"assert r3 == 'visit now!', f'r3: {repr(r3)}'\n",
|
| 632 |
+
"\n",
|
| 633 |
+
"print('clean_text tests passed')\n",
|
| 634 |
+
"print(f' {repr(r1)}')\n",
|
| 635 |
+
"print(f' {repr(r2)}')\n",
|
| 636 |
+
"print(f' {repr(r3)}')"
|
| 637 |
+
]
|
| 638 |
+
},
|
| 639 |
+
{
|
| 640 |
+
"cell_type": "code",
|
| 641 |
+
"execution_count": null,
|
| 642 |
+
"id": "12",
|
| 643 |
+
"metadata": {},
|
| 644 |
+
"outputs": [],
|
| 645 |
+
"source": [
|
| 646 |
+
"# -- Cell 11: Whisper processor + prepare_dataset -----------------------------\n",
|
| 647 |
+
"# WhisperProcessor imports processing_utils -> image_utils -> torchvision,\n",
|
| 648 |
+
"# which crashes when torch/torchvision have mismatched CUDA versions.\n",
|
| 649 |
+
"# Fix: build the processor manually from its two sub-components.\n",
|
| 650 |
+
"# WhisperFeatureExtractor and WhisperTokenizer have no torchvision dependency.\n",
|
| 651 |
+
"import numpy as np\n",
|
| 652 |
+
"\n",
|
| 653 |
+
"from transformers.models.whisper.feature_extraction_whisper import WhisperFeatureExtractor\n",
|
| 654 |
+
"from transformers.models.whisper.tokenization_whisper import WhisperTokenizer\n",
|
| 655 |
+
"\n",
|
| 656 |
+
"print(f'Loading Whisper feature extractor + tokenizer: {WHISPER_MODEL_ID} ...')\n",
|
| 657 |
+
"_feat_ext = WhisperFeatureExtractor.from_pretrained(WHISPER_MODEL_ID, token=HF_TOKEN)\n",
|
| 658 |
+
"_tokenizer = WhisperTokenizer.from_pretrained(WHISPER_MODEL_ID, token=HF_TOKEN)\n",
|
| 659 |
+
"\n",
|
| 660 |
+
"\n",
|
| 661 |
+
"class _Processor:\n",
|
| 662 |
+
" \"\"\"Minimal WhisperProcessor substitute that avoids the torchvision import chain.\"\"\"\n",
|
| 663 |
+
" def __init__(self, feature_extractor, tokenizer):\n",
|
| 664 |
+
" self.feature_extractor = feature_extractor\n",
|
| 665 |
+
" self.tokenizer = tokenizer\n",
|
| 666 |
+
"\n",
|
| 667 |
+
" def get_decoder_prompt_ids(self, language, task='transcribe'):\n",
|
| 668 |
+
" return self.tokenizer.get_decoder_prompt_ids(language=language, task=task)\n",
|
| 669 |
+
"\n",
|
| 670 |
+
" def save_pretrained(self, path):\n",
|
| 671 |
+
" self.feature_extractor.save_pretrained(path)\n",
|
| 672 |
+
" self.tokenizer.save_pretrained(path)\n",
|
| 673 |
+
"\n",
|
| 674 |
+
"\n",
|
| 675 |
+
"processor = _Processor(_feat_ext, _tokenizer)\n",
|
| 676 |
+
"print('Processor ready')\n",
|
| 677 |
+
"\n",
|
| 678 |
+
"\n",
|
| 679 |
+
"def prepare_dataset(batch, text_col='transcription', lang=TRAIN_LANG):\n",
|
| 680 |
+
" \"\"\"\n",
|
| 681 |
+
" Resample to 16 kHz, extract log-mel features, tokenise text.\n",
|
| 682 |
+
" Works on any dict with 'audio' (HF Audio column) and a text column.\n",
|
| 683 |
+
" \"\"\"\n",
|
| 684 |
+
" audio = batch['audio']\n",
|
| 685 |
+
" audio_array = np.array(audio['array'], dtype=np.float32)\n",
|
| 686 |
+
" orig_sr = audio['sampling_rate']\n",
|
| 687 |
+
"\n",
|
| 688 |
+
" if orig_sr != TARGET_SR:\n",
|
| 689 |
+
" try:\n",
|
| 690 |
+
" import torchaudio.functional as F_audio, torch\n",
|
| 691 |
+
" audio_array = F_audio.resample(\n",
|
| 692 |
+
" torch.from_numpy(audio_array).unsqueeze(0),\n",
|
| 693 |
+
" orig_sr, TARGET_SR,\n",
|
| 694 |
+
" ).squeeze(0).numpy()\n",
|
| 695 |
+
" except Exception:\n",
|
| 696 |
+
" import librosa\n",
|
| 697 |
+
" audio_array = librosa.resample(audio_array, orig_sr=orig_sr, target_sr=TARGET_SR)\n",
|
| 698 |
+
"\n",
|
| 699 |
+
" batch['input_features'] = processor.feature_extractor(\n",
|
| 700 |
+
" audio_array, sampling_rate=TARGET_SR\n",
|
| 701 |
+
" ).input_features[0]\n",
|
| 702 |
+
"\n",
|
| 703 |
+
" raw_text = batch.get(text_col, '') or ''\n",
|
| 704 |
+
" _norm_text = _bam_norm(str(raw_text)) if lang == 'bam' else (_normalize_pular(str(raw_text)) if lang == 'ful' else str(raw_text))\n",
|
| 705 |
+
" cleaned = clean_text(_norm_text, lang=lang)\n",
|
| 706 |
+
" batch['labels'] = processor.tokenizer(cleaned).input_ids\n",
|
| 707 |
+
" return batch\n",
|
| 708 |
+
"\n",
|
| 709 |
+
"\n",
|
| 710 |
+
"print('prepare_dataset ready')"
|
| 711 |
+
]
|
| 712 |
+
},
|
| 713 |
+
{
|
| 714 |
+
"cell_type": "code",
|
| 715 |
+
"execution_count": null,
|
| 716 |
+
"id": "13",
|
| 717 |
+
"metadata": {},
|
| 718 |
+
"outputs": [],
|
| 719 |
+
"source": [
|
| 720 |
+
"# -- Cell 12: Build & merge all datasets --------------------------------------\n",
|
| 721 |
+
"from datasets import Dataset, Audio as HFAudio, concatenate_datasets\n",
|
| 722 |
+
"from functools import partial\n",
|
| 723 |
+
"\n",
|
| 724 |
+
"train_ds = None # set here so Cell 12b can detect whether we succeeded\n",
|
| 725 |
+
"eval_ds = None\n",
|
| 726 |
+
"all_parts = []\n",
|
| 727 |
+
"\n",
|
| 728 |
+
"# -- Part A: User corrections -------------------------------------------------\n",
|
| 729 |
+
"if correction_records:\n",
|
| 730 |
+
" print(f'Part A: {len(correction_records)} corrections x {CORRECTION_REPEAT}')\n",
|
| 731 |
+
" rec_list = correction_records * CORRECTION_REPEAT\n",
|
| 732 |
+
" corr_ds = Dataset.from_dict({\n",
|
| 733 |
+
" 'audio': [r['local_audio'] for r in rec_list],\n",
|
| 734 |
+
" 'transcription': [r.get('corrected_text') or r.get('transcription', '') for r in rec_list],\n",
|
| 735 |
+
" })\n",
|
| 736 |
+
" corr_ds = corr_ds.cast_column('audio', HFAudio(sampling_rate=TARGET_SR))\n",
|
| 737 |
+
" corr_ds = corr_ds.map(\n",
|
| 738 |
+
" partial(prepare_dataset, text_col='transcription'),\n",
|
| 739 |
+
" remove_columns=corr_ds.column_names,\n",
|
| 740 |
+
" )\n",
|
| 741 |
+
" all_parts.append(corr_ds)\n",
|
| 742 |
+
" print(f' -> {len(corr_ds)} samples')\n",
|
| 743 |
+
"else:\n",
|
| 744 |
+
" print('Part A: no corrections -- skipping')\n",
|
| 745 |
+
"\n",
|
| 746 |
+
"# -- Part B: WaxalNLP ---------------------------------------------------------\n",
|
| 747 |
+
"if waxal_ds is not None:\n",
|
| 748 |
+
" print(f'Part B: materialising up to {MAX_WAXAL_TRAIN} WaxalNLP samples ...')\n",
|
| 749 |
+
" waxal_rows = list(waxal_ds.take(MAX_WAXAL_TRAIN))\n",
|
| 750 |
+
" waxal_local = Dataset.from_dict({\n",
|
| 751 |
+
" 'audio': [r['audio'] for r in waxal_rows],\n",
|
| 752 |
+
" 'transcription': [r.get(WAXAL_TEXT_COL, '') for r in waxal_rows],\n",
|
| 753 |
+
" })\n",
|
| 754 |
+
" waxal_local = waxal_local.cast_column('audio', HFAudio(sampling_rate=TARGET_SR))\n",
|
| 755 |
+
" waxal_local = waxal_local.map(\n",
|
| 756 |
+
" partial(prepare_dataset, text_col='transcription'),\n",
|
| 757 |
+
" remove_columns=waxal_local.column_names,\n",
|
| 758 |
+
" )\n",
|
| 759 |
+
" all_parts.append(waxal_local)\n",
|
| 760 |
+
" print(f' -> {len(waxal_local)} samples')\n",
|
| 761 |
+
"else:\n",
|
| 762 |
+
" print('Part B: WaxalNLP not available -- skipping')\n",
|
| 763 |
+
"\n",
|
| 764 |
+
"# -- Part C: External datasets ------------------------------------------------\n",
|
| 765 |
+
"for ext_ds, text_col in external_datasets:\n",
|
| 766 |
+
" ext_rows = list(ext_ds)\n",
|
| 767 |
+
" if not ext_rows:\n",
|
| 768 |
+
" continue\n",
|
| 769 |
+
" ext_local = Dataset.from_dict({\n",
|
| 770 |
+
" 'audio': [r['audio'] for r in ext_rows],\n",
|
| 771 |
+
" 'transcription': [r.get(text_col, '') for r in ext_rows],\n",
|
| 772 |
+
" })\n",
|
| 773 |
+
" ext_local = ext_local.cast_column('audio', HFAudio(sampling_rate=TARGET_SR))\n",
|
| 774 |
+
" ext_local = ext_local.map(\n",
|
| 775 |
+
" partial(prepare_dataset, text_col='transcription'),\n",
|
| 776 |
+
" remove_columns=ext_local.column_names,\n",
|
| 777 |
+
" )\n",
|
| 778 |
+
" all_parts.append(ext_local)\n",
|
| 779 |
+
" print(f'Part C: {len(ext_local)} external samples')\n",
|
| 780 |
+
"\n",
|
| 781 |
+
"# -- Result -------------------------------------------------------------------\n",
|
| 782 |
+
"print(f'\\nData summary: {len(all_parts)} source(s) loaded')\n",
|
| 783 |
+
"\n",
|
| 784 |
+
"if not all_parts:\n",
|
| 785 |
+
" print('No real data available for Bambara yet.')\n",
|
| 786 |
+
" print('Cell 12b (below) will build a synthetic dataset so the full')\n",
|
| 787 |
+
" print('training pipeline can be validated. Switch to TRAIN_LANG=\"ful\"')\n",
|
| 788 |
+
" print('in Cell 3 for a real Fula training run using WaxalNLP.')\n",
|
| 789 |
+
"else:\n",
|
| 790 |
+
" combined = concatenate_datasets(all_parts).shuffle(seed=42)\n",
|
| 791 |
+
" n_eval = max(1, min(int(0.05 * len(combined)), 200))\n",
|
| 792 |
+
" split = combined.train_test_split(test_size=n_eval)\n",
|
| 793 |
+
" train_ds = split['train']\n",
|
| 794 |
+
" eval_ds = split['test']\n",
|
| 795 |
+
" print(f'Train: {len(train_ds)} Eval: {len(eval_ds)}')\n",
|
| 796 |
+
"# Auto-cap MAX_STEPS: no point running 4000 steps on a tiny dataset.\n",
|
| 797 |
+
"# Rule: at least 20 passes through the data, capped at user's MAX_STEPS.\n",
|
| 798 |
+
"if train_ds is not None:\n",
|
| 799 |
+
" steps_per_epoch = max(1, len(train_ds) // (BATCH_SIZE * GRAD_ACCUM))\n",
|
| 800 |
+
" _auto_steps = max(200, steps_per_epoch * 20)\n",
|
| 801 |
+
" if _auto_steps < MAX_STEPS:\n",
|
| 802 |
+
" print(f'Auto-capping MAX_STEPS {MAX_STEPS} -> {_auto_steps} (small dataset)')\n",
|
| 803 |
+
" MAX_STEPS = _auto_steps\n",
|
| 804 |
+
" else:\n",
|
| 805 |
+
" print(f'MAX_STEPS={MAX_STEPS} OK for {len(train_ds)} training samples')"
|
| 806 |
+
]
|
| 807 |
+
},
|
| 808 |
+
{
|
| 809 |
+
"cell_type": "code",
|
| 810 |
+
"execution_count": null,
|
| 811 |
+
"id": "14",
|
| 812 |
+
"metadata": {},
|
| 813 |
+
"outputs": [],
|
| 814 |
+
"source": [
|
| 815 |
+
"# -- Cell 12b: Synthetic fallback (SKIP if Cell 12 succeeded) ----------------\n",
|
| 816 |
+
"# Run this cell ONLY if Cell 12 raised \"No data loaded\".\n",
|
| 817 |
+
"# Generates 50 short silent audio samples labelled with vocabulary.jsonl\n",
|
| 818 |
+
"# entries so training can proceed and you can verify the pipeline works.\n",
|
| 819 |
+
"# Replace with real data (accept Common Voice terms, or add corrections) for\n",
|
| 820 |
+
"# a meaningful model.\n",
|
| 821 |
+
"\n",
|
| 822 |
+
"import numpy as np\n",
|
| 823 |
+
"from datasets import Dataset, Audio as HFAudio, concatenate_datasets\n",
|
| 824 |
+
"from functools import partial\n",
|
| 825 |
+
"\n",
|
| 826 |
+
"if 'train_ds' in dir() and train_ds is not None:\n",
|
| 827 |
+
" print('Cell 12 succeeded -- nothing to do here.')\n",
|
| 828 |
+
"else:\n",
|
| 829 |
+
" print('Building synthetic fallback dataset from vocabulary.jsonl ...')\n",
|
| 830 |
+
"\n",
|
| 831 |
+
" # Use vocab entries if available, otherwise generic phrases\n",
|
| 832 |
+
" if vocab_entries:\n",
|
| 833 |
+
" phrases = [e.get('word', 'test') for e in vocab_entries[:50]]\n",
|
| 834 |
+
" else:\n",
|
| 835 |
+
" phrases = [f'word {i}' for i in range(50)]\n",
|
| 836 |
+
"\n",
|
| 837 |
+
" SR = TARGET_SR\n",
|
| 838 |
+
" rows = []\n",
|
| 839 |
+
" for phrase in phrases:\n",
|
| 840 |
+
" # 1-second silent audio (safe baseline for feature extraction)\n",
|
| 841 |
+
" audio_array = np.zeros(SR, dtype=np.float32)\n",
|
| 842 |
+
" rows.append({'audio_array': audio_array, 'transcription': phrase})\n",
|
| 843 |
+
"\n",
|
| 844 |
+
" # Build dataset directly from numpy arrays\n",
|
| 845 |
+
" synth_ds = Dataset.from_dict({\n",
|
| 846 |
+
" 'transcription': [r['transcription'] for r in rows],\n",
|
| 847 |
+
" })\n",
|
| 848 |
+
"\n",
|
| 849 |
+
" # Add audio column manually\n",
|
| 850 |
+
" def _add_audio(batch, idx):\n",
|
| 851 |
+
" batch['input_features'] = processor.feature_extractor(\n",
|
| 852 |
+
" np.zeros(TARGET_SR, dtype=np.float32), sampling_rate=TARGET_SR\n",
|
| 853 |
+
" ).input_features[0]\n",
|
| 854 |
+
" cleaned = clean_text(rows[idx]['transcription'], lang=TRAIN_LANG)\n",
|
| 855 |
+
" batch['labels'] = processor.tokenizer(cleaned).input_ids\n",
|
| 856 |
+
" return batch\n",
|
| 857 |
+
"\n",
|
| 858 |
+
" synth_processed = synth_ds.map(\n",
|
| 859 |
+
" _add_audio,\n",
|
| 860 |
+
" with_indices=True,\n",
|
| 861 |
+
" remove_columns=synth_ds.column_names,\n",
|
| 862 |
+
" )\n",
|
| 863 |
+
"\n",
|
| 864 |
+
" split = synth_processed.train_test_split(test_size=0.1, seed=42)\n",
|
| 865 |
+
" train_ds = split['train']\n",
|
| 866 |
+
" eval_ds = split['test']\n",
|
| 867 |
+
" print(f'Synthetic fallback: {len(train_ds)} train, {len(eval_ds)} eval')\n",
|
| 868 |
+
" print('WARNING: training on synthetic data produces a non-functional model.')\n",
|
| 869 |
+
" print('Accept Common Voice terms and re-run Cell 9 + Cell 12 for real data.')"
|
| 870 |
+
]
|
| 871 |
+
},
|
| 872 |
+
{
|
| 873 |
+
"cell_type": "markdown",
|
| 874 |
+
"id": "15",
|
| 875 |
+
"metadata": {},
|
| 876 |
+
"source": [
|
| 877 |
+
"---\n",
|
| 878 |
+
"## Model Setup - Partial Freeze Fine-Tuning\n",
|
| 879 |
+
"\n",
|
| 880 |
+
"openai/whisper-small is loaded in fp16. All parameters are frozen, then\n",
|
| 881 |
+
"the last 2 decoder layers + layer norm + output projection are unfrozen (~5%\n",
|
| 882 |
+
"trainable params). No PEFT/LoRA -- avoids PEFT/transformers 5.x\n",
|
| 883 |
+
"incompatibility where input_ids is passed twice to WhisperDecoder.\n"
|
| 884 |
+
]
|
| 885 |
+
},
|
| 886 |
+
{
|
| 887 |
+
"cell_type": "code",
|
| 888 |
+
"execution_count": null,
|
| 889 |
+
"id": "16",
|
| 890 |
+
"metadata": {},
|
| 891 |
+
"outputs": [],
|
| 892 |
+
"source": [
|
| 893 |
+
"# -- Cell 13: Load Whisper-small (fp16) + freeze most layers ------------------\n",
|
| 894 |
+
"# PEFT LoRA causes TypeError with transformers 5.x regardless of which layers\n",
|
| 895 |
+
"# are targeted: PeftModelForSeq2SeqLM wraps the entire model in BaseTuner whose\n",
|
| 896 |
+
"# forward(*args, **kwargs) passes input_ids in a way that causes WhisperDecoder\n",
|
| 897 |
+
"# to receive it twice. Fix: skip PEFT entirely. Freeze all params, unfreeze\n",
|
| 898 |
+
"# last 2 decoder layers (~5% trainable params -- same capacity as LoRA r=32).\n",
|
| 899 |
+
"import torch\n",
|
| 900 |
+
"from transformers.models.whisper.modeling_whisper import WhisperForConditionalGeneration\n",
|
| 901 |
+
"\n",
|
| 902 |
+
"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
|
| 903 |
+
"print(f\"Loading {WHISPER_MODEL_ID} in fp32 on {device} ...\")\n",
|
| 904 |
+
"\n",
|
| 905 |
+
"model = WhisperForConditionalGeneration.from_pretrained(\n",
|
| 906 |
+
" WHISPER_MODEL_ID,\n",
|
| 907 |
+
" torch_dtype=torch.float32, # fp32 storage -- AMP casts internally during training\n",
|
| 908 |
+
" token=HF_TOKEN,\n",
|
| 909 |
+
")\n",
|
| 910 |
+
"model = model.to(device)\n",
|
| 911 |
+
"\n",
|
| 912 |
+
"# Force target language -- avoids language-detection overhead during training\n",
|
| 913 |
+
"# Move generation params to GenerationConfig (avoids deprecation warning)\n",
|
| 914 |
+
"_dec_ids = processor.get_decoder_prompt_ids(language='fr', task='transcribe')\n",
|
| 915 |
+
"model.generation_config.forced_decoder_ids = _dec_ids\n",
|
| 916 |
+
"model.generation_config.suppress_tokens = []\n",
|
| 917 |
+
"model.config.use_cache = False # required for gradient checkpointing\n",
|
| 918 |
+
"\n",
|
| 919 |
+
"# ── Freeze all params, then selectively unfreeze ─────────────────────────────\n",
|
| 920 |
+
"for param in model.parameters():\n",
|
| 921 |
+
" param.requires_grad = False\n",
|
| 922 |
+
"\n",
|
| 923 |
+
"# Unfreeze last 2 decoder layers + final layer norm + output projection.\n",
|
| 924 |
+
"# These handle language-specific token generation.\n",
|
| 925 |
+
"for module in [\n",
|
| 926 |
+
" model.model.decoder.layers[-2],\n",
|
| 927 |
+
" model.model.decoder.layers[-1],\n",
|
| 928 |
+
" model.model.decoder.layer_norm,\n",
|
| 929 |
+
" model.proj_out,\n",
|
| 930 |
+
"]:\n",
|
| 931 |
+
" for param in module.parameters():\n",
|
| 932 |
+
" param.requires_grad = True\n",
|
| 933 |
+
"\n",
|
| 934 |
+
"trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 935 |
+
"total = sum(p.numel() for p in model.parameters())\n",
|
| 936 |
+
"print(f\"Trainable params: {trainable:,} / {total:,} ({100*trainable/total:.1f}%)\")\n",
|
| 937 |
+
"\n",
|
| 938 |
+
"# gradient_checkpointing enabled via TrainingArguments below (args handle enable/disable)\n",
|
| 939 |
+
"model.train()\n",
|
| 940 |
+
"\n",
|
| 941 |
+
"vram_mb = torch.cuda.memory_allocated() / 1e6 if torch.cuda.is_available() else 0\n",
|
| 942 |
+
"total_vram = torch.cuda.get_device_properties(0).total_memory / 1e6 if torch.cuda.is_available() else 0\n",
|
| 943 |
+
"print(f\"VRAM used: {vram_mb:.0f} MB / {total_vram:.0f} MB\")\n",
|
| 944 |
+
"print(f\"Model ready on {device}\")\n"
|
| 945 |
+
]
|
| 946 |
+
},
|
| 947 |
+
{
|
| 948 |
+
"cell_type": "code",
|
| 949 |
+
"execution_count": null,
|
| 950 |
+
"id": "17",
|
| 951 |
+
"metadata": {},
|
| 952 |
+
"outputs": [],
|
| 953 |
+
"source": [
|
| 954 |
+
"# -- Cell 14: Data collator + CER metric --------------------------------------\n",
|
| 955 |
+
"import jiwer\n",
|
| 956 |
+
"from dataclasses import dataclass\n",
|
| 957 |
+
"from typing import Any, Dict, List\n",
|
| 958 |
+
"\n",
|
| 959 |
+
"transform = jiwer.Compose([\n",
|
| 960 |
+
" jiwer.ToLowerCase(),\n",
|
| 961 |
+
" jiwer.RemoveMultipleSpaces(),\n",
|
| 962 |
+
" jiwer.Strip(),\n",
|
| 963 |
+
" jiwer.RemovePunctuation(),\n",
|
| 964 |
+
" jiwer.ReduceToListOfListOfWords(),\n",
|
| 965 |
+
"])\n",
|
| 966 |
+
"\n",
|
| 967 |
+
"# CER transform (no word-split step needed)\n",
|
| 968 |
+
"_cer_transform = jiwer.Compose([\n",
|
| 969 |
+
" jiwer.ToLowerCase(),\n",
|
| 970 |
+
" jiwer.RemoveMultipleSpaces(),\n",
|
| 971 |
+
" jiwer.Strip(),\n",
|
| 972 |
+
" jiwer.RemovePunctuation(),\n",
|
| 973 |
+
"])\n",
|
| 974 |
+
"\n",
|
| 975 |
+
"\n",
|
| 976 |
+
"@dataclass\n",
|
| 977 |
+
"class DataCollatorSpeechSeq2SeqWithPadding:\n",
|
| 978 |
+
" processor: Any\n",
|
| 979 |
+
"\n",
|
| 980 |
+
" def __call__(self, features: List[Dict]) -> Dict:\n",
|
| 981 |
+
" import torch\n",
|
| 982 |
+
" input_feats = [{'input_features': f['input_features']} for f in features]\n",
|
| 983 |
+
" batch = self.processor.feature_extractor.pad(input_feats, return_tensors='pt')\n",
|
| 984 |
+
"\n",
|
| 985 |
+
" # Leave features in fp32 -- AMP (fp16=True in TrainingArgs) handles casting\n",
|
| 986 |
+
"\n",
|
| 987 |
+
" label_feats = [{'input_ids': f['labels']} for f in features]\n",
|
| 988 |
+
" labels_batch = self.processor.tokenizer.pad(label_feats, return_tensors='pt')\n",
|
| 989 |
+
" labels = labels_batch['input_ids'].masked_fill(\n",
|
| 990 |
+
" labels_batch.attention_mask.ne(1), -100\n",
|
| 991 |
+
" )\n",
|
| 992 |
+
" if (labels[:, 0] == self.processor.tokenizer.bos_token_id).all().item():\n",
|
| 993 |
+
" labels = labels[:, 1:]\n",
|
| 994 |
+
" batch['labels'] = labels\n",
|
| 995 |
+
" return batch\n",
|
| 996 |
+
"\n",
|
| 997 |
+
"\n",
|
| 998 |
+
"def _apply_jiwer_transform(texts, t):\n",
|
| 999 |
+
" \"\"\"Apply a jiwer Compose transform and return plain strings (not nested lists).\"\"\"\n",
|
| 1000 |
+
" import re as _re\n",
|
| 1001 |
+
" result = []\n",
|
| 1002 |
+
" for s in texts:\n",
|
| 1003 |
+
" s = s.lower()\n",
|
| 1004 |
+
" s = _re.sub(r'[^\\w\\s]', '', s) # RemovePunctuation equivalent\n",
|
| 1005 |
+
" s = ' '.join(s.split()) # RemoveMultipleSpaces + Strip\n",
|
| 1006 |
+
" result.append(s)\n",
|
| 1007 |
+
" return result\n",
|
| 1008 |
+
"\n",
|
| 1009 |
+
"\n",
|
| 1010 |
+
"def compute_metrics(pred):\n",
|
| 1011 |
+
" pred_ids = pred.predictions\n",
|
| 1012 |
+
" label_ids = pred.label_ids\n",
|
| 1013 |
+
" label_ids[label_ids == -100] = processor.tokenizer.pad_token_id\n",
|
| 1014 |
+
"\n",
|
| 1015 |
+
" pred_str = processor.tokenizer.batch_decode(pred_ids, skip_special_tokens=True)\n",
|
| 1016 |
+
" label_str = processor.tokenizer.batch_decode(label_ids, skip_special_tokens=True)\n",
|
| 1017 |
+
"\n",
|
| 1018 |
+
" # Pre-normalise so we can filter empties AFTER the transform.\n",
|
| 1019 |
+
" # A reference like \"?\" or \"1\" decodes to non-empty but becomes empty\n",
|
| 1020 |
+
" # after punctuation/number removal -- jiwer crashes on empty references.\n",
|
| 1021 |
+
" norm_ref = _apply_jiwer_transform(label_str, _cer_transform)\n",
|
| 1022 |
+
" norm_hyp = _apply_jiwer_transform(pred_str, _cer_transform)\n",
|
| 1023 |
+
" pairs = [(r, h) for r, h in zip(norm_ref, norm_hyp) if r.strip()]\n",
|
| 1024 |
+
" if not pairs:\n",
|
| 1025 |
+
" return {'cer': 0.0, 'wer': 0.0}\n",
|
| 1026 |
+
" ref_clean, hyp_clean = zip(*pairs)\n",
|
| 1027 |
+
"\n",
|
| 1028 |
+
" cer = jiwer.cer(list(ref_clean), list(hyp_clean)) # already normalised\n",
|
| 1029 |
+
" wer = jiwer.wer(list(ref_clean), list(hyp_clean)) # already normalised\n",
|
| 1030 |
+
" return {'cer': round(cer, 4), 'wer': round(wer, 4)}\n",
|
| 1031 |
+
"\n",
|
| 1032 |
+
"\n",
|
| 1033 |
+
"collator = DataCollatorSpeechSeq2SeqWithPadding(processor=processor)\n",
|
| 1034 |
+
"print('Collator and WER metric ready')"
|
| 1035 |
+
]
|
| 1036 |
+
},
|
| 1037 |
+
{
|
| 1038 |
+
"cell_type": "markdown",
|
| 1039 |
+
"id": "18",
|
| 1040 |
+
"metadata": {},
|
| 1041 |
+
"source": [
|
| 1042 |
+
"---\n",
|
| 1043 |
+
"## Training — Deep Sleep 😴\n",
|
| 1044 |
+
"\n",
|
| 1045 |
+
"The trainer runs for `MAX_STEPS` steps, saving a checkpoint every `SAVE_STEPS`.\n",
|
| 1046 |
+
"If the Kaggle session times out and you re-run the notebook, Cell 6 will detect\n",
|
| 1047 |
+
"the latest checkpoint and `trainer.train()` will resume automatically — no progress lost."
|
| 1048 |
+
]
|
| 1049 |
+
},
|
| 1050 |
+
{
|
| 1051 |
+
"cell_type": "code",
|
| 1052 |
+
"execution_count": null,
|
| 1053 |
+
"id": "19",
|
| 1054 |
+
"metadata": {},
|
| 1055 |
+
"outputs": [],
|
| 1056 |
+
"source": [
|
| 1057 |
+
"# -- Cell 15: Training arguments ----------------------------------------------\n",
|
| 1058 |
+
"import inspect\n",
|
| 1059 |
+
"from transformers import Seq2SeqTrainingArguments\n",
|
| 1060 |
+
"\n",
|
| 1061 |
+
"# transformers 4.x used 'evaluation_strategy'; 4.45+ renamed to 'eval_strategy'.\n",
|
| 1062 |
+
"# Detect which name this installed version accepts.\n",
|
| 1063 |
+
"_params = inspect.signature(Seq2SeqTrainingArguments.__init__).parameters\n",
|
| 1064 |
+
"_eval_key = 'eval_strategy' if 'eval_strategy' in _params else 'evaluation_strategy'\n",
|
| 1065 |
+
"\n",
|
| 1066 |
+
"training_args = Seq2SeqTrainingArguments(\n",
|
| 1067 |
+
" output_dir=OUTPUT_DIR,\n",
|
| 1068 |
+
"\n",
|
| 1069 |
+
" max_steps=MAX_STEPS,\n",
|
| 1070 |
+
" warmup_steps=WARMUP_STEPS,\n",
|
| 1071 |
+
" logging_steps=LOGGING_STEPS,\n",
|
| 1072 |
+
" save_steps=SAVE_STEPS,\n",
|
| 1073 |
+
" eval_steps=EVAL_STEPS,\n",
|
| 1074 |
+
"\n",
|
| 1075 |
+
" per_device_train_batch_size=BATCH_SIZE,\n",
|
| 1076 |
+
" per_device_eval_batch_size=8,\n",
|
| 1077 |
+
" gradient_accumulation_steps=GRAD_ACCUM,\n",
|
| 1078 |
+
"\n",
|
| 1079 |
+
" fp16=True,\n",
|
| 1080 |
+
" gradient_checkpointing=True, # reduces activation memory on T4\n",
|
| 1081 |
+
"\n",
|
| 1082 |
+
" learning_rate=LEARNING_RATE,\n",
|
| 1083 |
+
" lr_scheduler_type='cosine',\n",
|
| 1084 |
+
" weight_decay=0.0,\n",
|
| 1085 |
+
" adam_beta1=0.9,\n",
|
| 1086 |
+
" adam_beta2=0.98,\n",
|
| 1087 |
+
" adam_epsilon=1e-6,\n",
|
| 1088 |
+
"\n",
|
| 1089 |
+
" **{_eval_key: 'steps'},\n",
|
| 1090 |
+
" predict_with_generate=True,\n",
|
| 1091 |
+
" generation_max_length=225,\n",
|
| 1092 |
+
" load_best_model_at_end=True,\n",
|
| 1093 |
+
" metric_for_best_model='cer',\n",
|
| 1094 |
+
" greater_is_better=False,\n",
|
| 1095 |
+
"\n",
|
| 1096 |
+
" save_total_limit=3,\n",
|
| 1097 |
+
" save_strategy='steps',\n",
|
| 1098 |
+
"\n",
|
| 1099 |
+
" report_to=['tensorboard'], # tensorboard logs to OUTPUT_DIR/runs by default\n",
|
| 1100 |
+
" push_to_hub=False,\n",
|
| 1101 |
+
")\n",
|
| 1102 |
+
"\n",
|
| 1103 |
+
"print(f'Training arguments ready (using {_eval_key}=steps)')\n",
|
| 1104 |
+
"print(f' Effective batch size: {BATCH_SIZE * GRAD_ACCUM}')\n",
|
| 1105 |
+
"print(f' Max steps : {MAX_STEPS}')\n"
|
| 1106 |
+
]
|
| 1107 |
+
},
|
| 1108 |
+
{
|
| 1109 |
+
"cell_type": "code",
|
| 1110 |
+
"execution_count": null,
|
| 1111 |
+
"id": "20",
|
| 1112 |
+
"metadata": {},
|
| 1113 |
+
"outputs": [],
|
| 1114 |
+
"source": [
|
| 1115 |
+
"# -- Cell 16: TRAIN -----------------------------------------------------------\n",
|
| 1116 |
+
"from transformers import Seq2SeqTrainer\n",
|
| 1117 |
+
"\n",
|
| 1118 |
+
"trainer = Seq2SeqTrainer(\n",
|
| 1119 |
+
" model=model,\n",
|
| 1120 |
+
" args=training_args,\n",
|
| 1121 |
+
" train_dataset=train_ds,\n",
|
| 1122 |
+
" eval_dataset=eval_ds,\n",
|
| 1123 |
+
" data_collator=collator,\n",
|
| 1124 |
+
" compute_metrics=compute_metrics,\n",
|
| 1125 |
+
" # 'tokenizer' argument removed -- renamed to 'processing_class' in\n",
|
| 1126 |
+
" # transformers 5.x and passing the old name raises TypeError\n",
|
| 1127 |
+
")\n",
|
| 1128 |
+
"\n",
|
| 1129 |
+
"print(f'Starting training ...')\n",
|
| 1130 |
+
"print(f' Resume from : {LAST_CHECKPOINT or \"scratch\"}')\n",
|
| 1131 |
+
"print(f' Train size : {len(train_ds)}')\n",
|
| 1132 |
+
"print(f' Eval size : {len(eval_ds)}')\n",
|
| 1133 |
+
"\n",
|
| 1134 |
+
"train_result = trainer.train(resume_from_checkpoint=LAST_CHECKPOINT)\n",
|
| 1135 |
+
"\n",
|
| 1136 |
+
"print('\\nTraining complete')\n",
|
| 1137 |
+
"print(f' Steps : {train_result.global_step}')\n",
|
| 1138 |
+
"print(f' Train loss: {train_result.training_loss:.4f}')\n",
|
| 1139 |
+
"\n",
|
| 1140 |
+
"trainer.save_model(OUTPUT_DIR)\n",
|
| 1141 |
+
"model.generation_config.save_pretrained(OUTPUT_DIR)\n",
|
| 1142 |
+
"processor.save_pretrained(OUTPUT_DIR)\n",
|
| 1143 |
+
"print(f' Adapter saved -> {OUTPUT_DIR}')"
|
| 1144 |
+
]
|
| 1145 |
+
},
|
| 1146 |
+
{
|
| 1147 |
+
"cell_type": "markdown",
|
| 1148 |
+
"id": "21",
|
| 1149 |
+
"metadata": {},
|
| 1150 |
+
"source": [
|
| 1151 |
+
"---\n",
|
| 1152 |
+
"## Evaluation\n",
|
| 1153 |
+
"\n",
|
| 1154 |
+
"WER (Word Error Rate) is computed on the held-out eval split.\n",
|
| 1155 |
+
"A lower WER means fewer transcription mistakes. For Bambara/Fula on whisper-small,\n",
|
| 1156 |
+
"a WER < 40% is a strong result given the limited training data."
|
| 1157 |
+
]
|
| 1158 |
+
},
|
| 1159 |
+
{
|
| 1160 |
+
"cell_type": "code",
|
| 1161 |
+
"execution_count": null,
|
| 1162 |
+
"id": "22",
|
| 1163 |
+
"metadata": {},
|
| 1164 |
+
"outputs": [],
|
| 1165 |
+
"source": [
|
| 1166 |
+
"# ── Cell 17: WER evaluation ───────────────────────────────────────────────────\n",
|
| 1167 |
+
"print('Running full evaluation on eval split ...')\n",
|
| 1168 |
+
"eval_results = trainer.evaluate()\n",
|
| 1169 |
+
"\n",
|
| 1170 |
+
"cer_score = eval_results.get('eval_cer', float('nan'))\n",
|
| 1171 |
+
"wer_score = eval_results.get('eval_wer', float('nan'))\n",
|
| 1172 |
+
"print(f'\\n✅ Final CER : {cer_score:.1%} (primary — lower is better)')\n",
|
| 1173 |
+
"print(f' Final WER : {wer_score:.1%} (secondary)')\n",
|
| 1174 |
+
"print(f' Eval loss : {eval_results.get(\"eval_loss\", float(\"nan\")):.4f}')\n",
|
| 1175 |
+
"# Show a few example transcriptions side-by-side\n",
|
| 1176 |
+
"import random, torch\n",
|
| 1177 |
+
"print('\\n── Sample predictions ───────────────────────────────')\n",
|
| 1178 |
+
"samples = random.sample(range(len(eval_ds)), min(5, len(eval_ds)))\n",
|
| 1179 |
+
"for idx in samples:\n",
|
| 1180 |
+
" item = eval_ds[idx]\n",
|
| 1181 |
+
" feats = torch.tensor(item['input_features']).unsqueeze(0).to(model.device)\n",
|
| 1182 |
+
" with torch.no_grad():\n",
|
| 1183 |
+
" pred_ids = model.generate(\n",
|
| 1184 |
+
" feats, # fp32 to match model dtype\n",
|
| 1185 |
+
" max_new_tokens=128,\n",
|
| 1186 |
+
" )\n",
|
| 1187 |
+
" pred_str = processor.tokenizer.batch_decode(pred_ids, skip_special_tokens=True)[0]\n",
|
| 1188 |
+
" labels = [t if t != -100 else processor.tokenizer.pad_token_id\n",
|
| 1189 |
+
" for t in item['labels']]\n",
|
| 1190 |
+
" ref_str = processor.tokenizer.decode(labels, skip_special_tokens=True)\n",
|
| 1191 |
+
" print(f' Ref : {ref_str}')\n",
|
| 1192 |
+
" print(f' Pred: {pred_str}')\n",
|
| 1193 |
+
" print()"
|
| 1194 |
+
]
|
| 1195 |
+
},
|
| 1196 |
+
{
|
| 1197 |
+
"cell_type": "markdown",
|
| 1198 |
+
"id": "23",
|
| 1199 |
+
"metadata": {},
|
| 1200 |
+
"source": [
|
| 1201 |
+
"---\n",
|
| 1202 |
+
"## Export — Push Fine-tuned Checkpoint to Hub\n",
|
| 1203 |
+
"\n",
|
| 1204 |
+
"The adapter is pushed to `ous-sow/sahel-agri-adapters` under the path \n",
|
| 1205 |
+
"`adapters/{lang_name}/` with a Git tag like `v1.2-bambara`.\n",
|
| 1206 |
+
"\n",
|
| 1207 |
+
"The version number is **auto-incremented** by reading existing tags on the repo\n",
|
| 1208 |
+
"so each training run gets a unique, traceable identifier."
|
| 1209 |
+
]
|
| 1210 |
+
},
|
| 1211 |
+
{
|
| 1212 |
+
"cell_type": "code",
|
| 1213 |
+
"execution_count": null,
|
| 1214 |
+
"id": "24",
|
| 1215 |
+
"metadata": {},
|
| 1216 |
+
"outputs": [],
|
| 1217 |
+
"source": [
|
| 1218 |
+
"# ── Cell 18: Compute next version tag ────────────────────────────────────────\n",
|
| 1219 |
+
"import re as _re\n",
|
| 1220 |
+
"from huggingface_hub import list_repo_refs\n",
|
| 1221 |
+
"\n",
|
| 1222 |
+
"def get_next_version_tag(repo_id: str, lang_name: str, hf_token: str) -> str:\n",
|
| 1223 |
+
" \"\"\"Auto-increment version tag: reads existing tags, bumps minor version.\"\"\"\n",
|
| 1224 |
+
" try:\n",
|
| 1225 |
+
" refs = list_repo_refs(repo_id, repo_type='model', token=hf_token)\n",
|
| 1226 |
+
" pattern = _re.compile(rf'^v(\\d+)\\.(\\d+)-{_re.escape(lang_name)}$')\n",
|
| 1227 |
+
" versions = []\n",
|
| 1228 |
+
" for tag in refs.tags:\n",
|
| 1229 |
+
" m = pattern.match(tag.name)\n",
|
| 1230 |
+
" if m:\n",
|
| 1231 |
+
" versions.append((int(m.group(1)), int(m.group(2))))\n",
|
| 1232 |
+
" if not versions:\n",
|
| 1233 |
+
" return f'v1.0-{lang_name}'\n",
|
| 1234 |
+
" major, minor = max(versions)\n",
|
| 1235 |
+
" return f'v{major}.{minor + 1}-{lang_name}'\n",
|
| 1236 |
+
" except Exception as e:\n",
|
| 1237 |
+
" print(f' Could not read existing tags ({e}) — defaulting to v1.0')\n",
|
| 1238 |
+
" return f'v1.0-{lang_name}'\n",
|
| 1239 |
+
"\n",
|
| 1240 |
+
"\n",
|
| 1241 |
+
"VERSION_TAG = get_next_version_tag(ADAPTER_REPO_ID, LANG_NAME, HF_TOKEN)\n",
|
| 1242 |
+
"PATH_IN_REPO = f'adapters/{LANG_NAME}'\n",
|
| 1243 |
+
"\n",
|
| 1244 |
+
"print(f'Version tag : {VERSION_TAG}')\n",
|
| 1245 |
+
"print(f'Path in repo : {ADAPTER_REPO_ID}/{PATH_IN_REPO}')"
|
| 1246 |
+
]
|
| 1247 |
+
},
|
| 1248 |
+
{
|
| 1249 |
+
"cell_type": "code",
|
| 1250 |
+
"execution_count": null,
|
| 1251 |
+
"id": "25",
|
| 1252 |
+
"metadata": {},
|
| 1253 |
+
"outputs": [],
|
| 1254 |
+
"source": [
|
| 1255 |
+
"# ── Cell 19: Push adapter to HF Model repo ───────────────────────────────────\n",
|
| 1256 |
+
"from huggingface_hub import HfApi, create_repo\n",
|
| 1257 |
+
"\n",
|
| 1258 |
+
"# Ensure repo exists\n",
|
| 1259 |
+
"create_repo(ADAPTER_REPO_ID, repo_type='model', private=True,\n",
|
| 1260 |
+
" exist_ok=True, token=HF_TOKEN)\n",
|
| 1261 |
+
"\n",
|
| 1262 |
+
"_cer_part = f'{cer_score:.1%}' if cer_score == cer_score else 'n/a'\n",
|
| 1263 |
+
"commit_msg = (\n",
|
| 1264 |
+
" f'[{VERSION_TAG}] {LANG_NAME} ({LANG_COUNTRY}) fine-tuned checkpoint — '\n",
|
| 1265 |
+
" f'{train_result.global_step} steps | CER {_cer_part} | '\n",
|
| 1266 |
+
" f'{len(correction_records)} corrections + WaxalNLP | {LANG_DIALECT}'\n",
|
| 1267 |
+
")\n",
|
| 1268 |
+
"\n",
|
| 1269 |
+
"api.upload_folder(\n",
|
| 1270 |
+
" folder_path=OUTPUT_DIR,\n",
|
| 1271 |
+
" repo_id=ADAPTER_REPO_ID,\n",
|
| 1272 |
+
" repo_type='model',\n",
|
| 1273 |
+
" path_in_repo=PATH_IN_REPO,\n",
|
| 1274 |
+
" commit_message=commit_msg,\n",
|
| 1275 |
+
")\n",
|
| 1276 |
+
"print(f'✅ Adapter uploaded: {ADAPTER_REPO_ID}/{PATH_IN_REPO}')\n",
|
| 1277 |
+
"\n",
|
| 1278 |
+
"# Create a Git tag for this version\n",
|
| 1279 |
+
"try:\n",
|
| 1280 |
+
" api.create_tag(\n",
|
| 1281 |
+
" repo_id=ADAPTER_REPO_ID,\n",
|
| 1282 |
+
" repo_type='model',\n",
|
| 1283 |
+
" tag=VERSION_TAG,\n",
|
| 1284 |
+
" tag_message=commit_msg,\n",
|
| 1285 |
+
" token=HF_TOKEN,\n",
|
| 1286 |
+
" )\n",
|
| 1287 |
+
" print(f'✅ Tag created : {VERSION_TAG}')\n",
|
| 1288 |
+
"except Exception as e:\n",
|
| 1289 |
+
" print(f'⚠️ Tag creation skipped: {e}')"
|
| 1290 |
+
]
|
| 1291 |
+
},
|
| 1292 |
+
{
|
| 1293 |
+
"cell_type": "code",
|
| 1294 |
+
"execution_count": null,
|
| 1295 |
+
"id": "26",
|
| 1296 |
+
"metadata": {},
|
| 1297 |
+
"outputs": [],
|
| 1298 |
+
"source": [
|
| 1299 |
+
"# ── Cell 20: Verification summary ────────────────────────────────────────────\n",
|
| 1300 |
+
"from huggingface_hub import list_repo_files\n",
|
| 1301 |
+
"\n",
|
| 1302 |
+
"print('=' * 60)\n",
|
| 1303 |
+
"print('DEEP SLEEP TRAINING — COMPLETE')\n",
|
| 1304 |
+
"print('=' * 60)\n",
|
| 1305 |
+
"print(f' Language : {TRAIN_LANG} ({LANG_NAME})')\n",
|
| 1306 |
+
"print(f' Model : {WHISPER_MODEL_ID}')\n",
|
| 1307 |
+
"print(f' Steps completed : {train_result.global_step}')\n",
|
| 1308 |
+
"print(f' Train loss : {train_result.training_loss:.4f}')\n",
|
| 1309 |
+
"_cer_disp = f'{cer_score:.1%}' if cer_score == cer_score else 'n/a'\n",
|
| 1310 |
+
"_wer_disp = f'{wer_score:.1%}' if wer_score == wer_score else 'n/a'\n",
|
| 1311 |
+
"print(f' Eval CER (primary) : {_cer_disp}')\n",
|
| 1312 |
+
"print(f' Eval WER (secondary): {_wer_disp}')\n",
|
| 1313 |
+
"print(f' Corrections used : {len(correction_records)} × {CORRECTION_REPEAT}')\n",
|
| 1314 |
+
"print(f' WaxalNLP samples : up to {MAX_WAXAL_TRAIN}')\n",
|
| 1315 |
+
"print(f' Version tag : {VERSION_TAG}')\n",
|
| 1316 |
+
"print(f' HF repo : {ADAPTER_REPO_ID}/{PATH_IN_REPO}')\n",
|
| 1317 |
+
"print()\n",
|
| 1318 |
+
"\n",
|
| 1319 |
+
"# List what was pushed\n",
|
| 1320 |
+
"try:\n",
|
| 1321 |
+
" repo_files = sorted(list_repo_files(\n",
|
| 1322 |
+
" ADAPTER_REPO_ID, repo_type='model', token=HF_TOKEN\n",
|
| 1323 |
+
" ))\n",
|
| 1324 |
+
" adapter_files = [f for f in repo_files if f.startswith(f'adapters/{LANG_NAME}/')]\n",
|
| 1325 |
+
" print('Adapter files in repo:')\n",
|
| 1326 |
+
" for f in adapter_files:\n",
|
| 1327 |
+
" print(f' {f}')\n",
|
| 1328 |
+
"except Exception as e:\n",
|
| 1329 |
+
" print(f'Could not list repo files: {e}')\n",
|
| 1330 |
+
"\n",
|
| 1331 |
+
"print()\n",
|
| 1332 |
+
"print('Next steps:')\n",
|
| 1333 |
+
"print(' 1. In your HF Space settings, confirm ADAPTER_REPO_ID secret is set')\n",
|
| 1334 |
+
"print(f' 2. Tab 3 → Reload Adapters → select \"{VERSION_TAG}\"')\n",
|
| 1335 |
+
"print(' 3. Collect more corrections in the Space, then re-run this notebook')"
|
| 1336 |
+
]
|
| 1337 |
+
}
|
| 1338 |
+
],
|
| 1339 |
+
"metadata": {
|
| 1340 |
+
"kernelspec": {
|
| 1341 |
+
"display_name": "Python 3",
|
| 1342 |
+
"language": "python",
|
| 1343 |
+
"name": "python3"
|
| 1344 |
+
},
|
| 1345 |
+
"language_info": {
|
| 1346 |
+
"name": "python",
|
| 1347 |
+
"version": "3.10.12"
|
| 1348 |
+
}
|
| 1349 |
+
},
|
| 1350 |
+
"nbformat": 4,
|
| 1351 |
+
"nbformat_minor": 5
|
| 1352 |
+
}
|
notebooks/{kernel-metadata.json → kaggle_master_trainer/kernel-metadata.json}
RENAMED
|
File without changes
|
notebooks/train_colab.ipynb
CHANGED
|
@@ -1,26 +1,8 @@
|
|
| 1 |
{
|
| 2 |
-
"nbformat": 4,
|
| 3 |
-
"nbformat_minor": 5,
|
| 4 |
-
"metadata": {
|
| 5 |
-
"kernelspec": {
|
| 6 |
-
"display_name": "Python 3",
|
| 7 |
-
"language": "python",
|
| 8 |
-
"name": "python3"
|
| 9 |
-
},
|
| 10 |
-
"language_info": {
|
| 11 |
-
"name": "python",
|
| 12 |
-
"version": "3.10.0"
|
| 13 |
-
},
|
| 14 |
-
"colab": {
|
| 15 |
-
"provenance": [],
|
| 16 |
-
"gpuType": "T4"
|
| 17 |
-
},
|
| 18 |
-
"accelerator": "GPU"
|
| 19 |
-
},
|
| 20 |
"cells": [
|
| 21 |
{
|
| 22 |
"cell_type": "markdown",
|
| 23 |
-
"id": "
|
| 24 |
"metadata": {},
|
| 25 |
"source": [
|
| 26 |
"# 🌾 Sahel-Agri Voice AI — Fine-tune on Farmer Feedback\n",
|
|
@@ -38,7 +20,7 @@
|
|
| 38 |
{
|
| 39 |
"cell_type": "code",
|
| 40 |
"execution_count": null,
|
| 41 |
-
"id": "
|
| 42 |
"metadata": {},
|
| 43 |
"outputs": [],
|
| 44 |
"source": [
|
|
@@ -54,7 +36,7 @@
|
|
| 54 |
{
|
| 55 |
"cell_type": "code",
|
| 56 |
"execution_count": null,
|
| 57 |
-
"id": "
|
| 58 |
"metadata": {},
|
| 59 |
"outputs": [],
|
| 60 |
"source": [
|
|
@@ -72,15 +54,40 @@
|
|
| 72 |
{
|
| 73 |
"cell_type": "code",
|
| 74 |
"execution_count": null,
|
| 75 |
-
"id": "
|
| 76 |
"metadata": {},
|
| 77 |
"outputs": [],
|
| 78 |
-
"source":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
},
|
| 80 |
{
|
| 81 |
"cell_type": "code",
|
| 82 |
"execution_count": null,
|
| 83 |
-
"id": "
|
| 84 |
"metadata": {},
|
| 85 |
"outputs": [],
|
| 86 |
"source": [
|
|
@@ -121,7 +128,7 @@
|
|
| 121 |
{
|
| 122 |
"cell_type": "code",
|
| 123 |
"execution_count": null,
|
| 124 |
-
"id": "
|
| 125 |
"metadata": {},
|
| 126 |
"outputs": [],
|
| 127 |
"source": [
|
|
@@ -159,7 +166,7 @@
|
|
| 159 |
{
|
| 160 |
"cell_type": "code",
|
| 161 |
"execution_count": null,
|
| 162 |
-
"id": "
|
| 163 |
"metadata": {},
|
| 164 |
"outputs": [],
|
| 165 |
"source": [
|
|
@@ -208,7 +215,7 @@
|
|
| 208 |
{
|
| 209 |
"cell_type": "code",
|
| 210 |
"execution_count": null,
|
| 211 |
-
"id": "
|
| 212 |
"metadata": {},
|
| 213 |
"outputs": [],
|
| 214 |
"source": [
|
|
@@ -235,7 +242,7 @@
|
|
| 235 |
{
|
| 236 |
"cell_type": "code",
|
| 237 |
"execution_count": null,
|
| 238 |
-
"id": "
|
| 239 |
"metadata": {},
|
| 240 |
"outputs": [],
|
| 241 |
"source": [
|
|
@@ -279,5 +286,23 @@
|
|
| 279 |
" print('ℹ️ No improvement on this single sample — collect more corrections and retrain.')"
|
| 280 |
]
|
| 281 |
}
|
| 282 |
-
]
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
"cells": [
|
| 3 |
{
|
| 4 |
"cell_type": "markdown",
|
| 5 |
+
"id": "0",
|
| 6 |
"metadata": {},
|
| 7 |
"source": [
|
| 8 |
"# 🌾 Sahel-Agri Voice AI — Fine-tune on Farmer Feedback\n",
|
|
|
|
| 20 |
{
|
| 21 |
"cell_type": "code",
|
| 22 |
"execution_count": null,
|
| 23 |
+
"id": "1",
|
| 24 |
"metadata": {},
|
| 25 |
"outputs": [],
|
| 26 |
"source": [
|
|
|
|
| 36 |
{
|
| 37 |
"cell_type": "code",
|
| 38 |
"execution_count": null,
|
| 39 |
+
"id": "2",
|
| 40 |
"metadata": {},
|
| 41 |
"outputs": [],
|
| 42 |
"source": [
|
|
|
|
| 54 |
{
|
| 55 |
"cell_type": "code",
|
| 56 |
"execution_count": null,
|
| 57 |
+
"id": "3",
|
| 58 |
"metadata": {},
|
| 59 |
"outputs": [],
|
| 60 |
+
"source": [
|
| 61 |
+
"# Cell 3 — HuggingFace login\n",
|
| 62 |
+
"# Colab: 🔑 icon (left sidebar) → Add new secret → name=HF_TOKEN\n",
|
| 63 |
+
"# Kaggle: Add Data → add as Kaggle secret named HF_TOKEN\n",
|
| 64 |
+
"import os\n",
|
| 65 |
+
"try:\n",
|
| 66 |
+
" from google.colab import userdata # type: ignore\n",
|
| 67 |
+
" HF_TOKEN = userdata.get('HF_TOKEN')\n",
|
| 68 |
+
"except Exception:\n",
|
| 69 |
+
" HF_TOKEN = os.environ.get('HF_TOKEN', '')\n",
|
| 70 |
+
"\n",
|
| 71 |
+
"if not HF_TOKEN:\n",
|
| 72 |
+
" raise ValueError('HF_TOKEN not found — see instructions above.')\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"from huggingface_hub import login\n",
|
| 75 |
+
"login(token=HF_TOKEN, add_to_git_credential=False)\n",
|
| 76 |
+
"\n",
|
| 77 |
+
"SPACE_REPO_ID = 'ous-sow/sahel-agri-voice'\n",
|
| 78 |
+
"FEEDBACK_REPO_ID = 'ous-sow/sahel-agri-feedback'\n",
|
| 79 |
+
"ADAPTER_REPO_ID = 'ous-sow/sahel-agri-adapters'\n",
|
| 80 |
+
"# Must match what the Space uses — whisper-small for cpu-basic, whisper-large-v3-turbo for GPU.\n",
|
| 81 |
+
"WHISPER_MODEL_ID = 'openai/whisper-small'\n",
|
| 82 |
+
"TRAIN_LANG = 'bam' # ← change to 'ful' for Fula\n",
|
| 83 |
+
"\n",
|
| 84 |
+
"print(f'✅ Logged in | training language: {TRAIN_LANG}')"
|
| 85 |
+
]
|
| 86 |
},
|
| 87 |
{
|
| 88 |
"cell_type": "code",
|
| 89 |
"execution_count": null,
|
| 90 |
+
"id": "4",
|
| 91 |
"metadata": {},
|
| 92 |
"outputs": [],
|
| 93 |
"source": [
|
|
|
|
| 128 |
{
|
| 129 |
"cell_type": "code",
|
| 130 |
"execution_count": null,
|
| 131 |
+
"id": "5",
|
| 132 |
"metadata": {},
|
| 133 |
"outputs": [],
|
| 134 |
"source": [
|
|
|
|
| 166 |
{
|
| 167 |
"cell_type": "code",
|
| 168 |
"execution_count": null,
|
| 169 |
+
"id": "6",
|
| 170 |
"metadata": {},
|
| 171 |
"outputs": [],
|
| 172 |
"source": [
|
|
|
|
| 215 |
{
|
| 216 |
"cell_type": "code",
|
| 217 |
"execution_count": null,
|
| 218 |
+
"id": "7",
|
| 219 |
"metadata": {},
|
| 220 |
"outputs": [],
|
| 221 |
"source": [
|
|
|
|
| 242 |
{
|
| 243 |
"cell_type": "code",
|
| 244 |
"execution_count": null,
|
| 245 |
+
"id": "8",
|
| 246 |
"metadata": {},
|
| 247 |
"outputs": [],
|
| 248 |
"source": [
|
|
|
|
| 286 |
" print('ℹ️ No improvement on this single sample — collect more corrections and retrain.')"
|
| 287 |
]
|
| 288 |
}
|
| 289 |
+
],
|
| 290 |
+
"metadata": {
|
| 291 |
+
"accelerator": "GPU",
|
| 292 |
+
"colab": {
|
| 293 |
+
"gpuType": "T4",
|
| 294 |
+
"provenance": []
|
| 295 |
+
},
|
| 296 |
+
"kernelspec": {
|
| 297 |
+
"display_name": "Python 3",
|
| 298 |
+
"language": "python",
|
| 299 |
+
"name": "python3"
|
| 300 |
+
},
|
| 301 |
+
"language_info": {
|
| 302 |
+
"name": "python",
|
| 303 |
+
"version": "3.10.0"
|
| 304 |
+
}
|
| 305 |
+
},
|
| 306 |
+
"nbformat": 4,
|
| 307 |
+
"nbformat_minor": 5
|
| 308 |
+
}
|
notebooks/train_fula_tts/kernel-metadata.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "ous-sow/sahel-voice-fula-tts-trainer",
|
| 3 |
+
"title": "Sahel Voice Fula TTS Trainer",
|
| 4 |
+
"code_file": "train_fula_tts.ipynb",
|
| 5 |
+
"language": "python",
|
| 6 |
+
"kernel_type": "notebook",
|
| 7 |
+
"is_private": true,
|
| 8 |
+
"enable_gpu": true,
|
| 9 |
+
"enable_tpu": false,
|
| 10 |
+
"enable_internet": true,
|
| 11 |
+
"dataset_sources": [],
|
| 12 |
+
"competition_sources": [],
|
| 13 |
+
"kernel_sources": []
|
| 14 |
+
}
|
notebooks/{train_fula_tts.ipynb → train_fula_tts/train_fula_tts.ipynb}
RENAMED
|
File without changes
|