{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "provenance": [], "name": "wakeword_training_template.ipynb", "gpuType": "T4" }, "accelerator": "GPU", "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Wake Word Training\n", "\n", "Basé sur `Automatic_model_training_simple_TFR`. \n", "Génération multi-langue : EN/EN-GB/DE/NL/IT/ES + FR (phonétique dédiée).\n", "\n", "**Runtime recommandé : GPU (T4)** — Durée estimée : ~45-90 min" ] }, { "cell_type": "code", "metadata": {}, "source": [ "from google.colab import drive\n", "drive.mount('/content/drive')\n", "\n", "import os\n", "drive_output_dir = '/content/drive/My Drive/Colab Notebooks/Wakeword/openwakeword'\n", "os.makedirs(drive_output_dir, exist_ok=True)\n", "print(f'Models will be saved in: {drive_output_dir}')" ], "outputs": [], "execution_count": null }, { "cell_type": "code", "metadata": {}, "source": "# @title Configuration globale { display-mode: \"form\" }\n# @markdown ### Wake word\n# @markdown `PHRASE_DEFAULT` : prononciation EN (utilise _ entre les mots)\n# @markdown `PHRASE_FR` : prononciation FR (avec accents si besoin)\n\nPHRASE_DEFAULT = 'hey_marco' # @param {type:\"string\"}\nPHRASE_FR = 'hé marco' # @param {type:\"string\"}\n\n# @markdown ---\n# @markdown ### Paramètres d'entraînement\n\nnumber_of_examples = 10000 # @param {type:\"slider\", min:1000, max:50000, step:500}\nnumber_of_training_steps = 25000 # @param {type:\"slider\", min:1000, max:50000, step:500}\nfalse_activation_penalty = 1500 # @param {type:\"slider\", min:100, max:3000, step:100}\n\n# @markdown ---\n# @markdown ### Répartition FR / EN (%)\n# @markdown Le reste (100% - FR%) va en EN. Mettre FR=0 pour full EN.\n\nfr_pct = 35 # @param {type:\"slider\", min:0, max:100, step:5}\n\n# @markdown ---\n# @markdown ### Objectifs qualité (early stopping)\n\ntarget_accuracy = 0.7 # @param {type:\"slider\", min:0.1, max:1.0, step:0.05}\ntarget_recall = 0.5 # @param {type:\"slider\", min:0.1, max:1.0, step:0.05}\n\n# ── Calcul automatique ────────────────────────────────────────────────────────\nimport math\n\nen_pct = 100 - fr_pct\nlang_samples = {\n 'fr': math.floor(number_of_examples * fr_pct / 100),\n 'en': number_of_examples - math.floor(number_of_examples * fr_pct / 100),\n}\n\nMODEL_NAME = PHRASE_DEFAULT.replace(' ', '_')\nCLIPS_DIR = f'./{MODEL_NAME}_clips'\n\nprint(f'Wake word : \"{PHRASE_DEFAULT}\" (EN) | \"{PHRASE_FR}\" (FR)')\nprint(f'Model name : {MODEL_NAME}')\nprint(f'Total samples : {sum(lang_samples.values())}')\nprint(f' fr : {lang_samples[\"fr\"]} ({fr_pct}%)')\nprint(f' en : {lang_samples[\"en\"]} ({en_pct}%)')", "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Étape 1 — Piper sample generator + modèles TTS" ] }, { "cell_type": "code", "metadata": {}, "source": [ "import os, sys\n", "from IPython.display import Audio, display\n", "\n", "if not os.path.exists('./piper-sample-generator'):\n", " !git clone https://github.com/rhasspy/piper-sample-generator\n", " !cd piper-sample-generator && git checkout 213d4d5\n", " !pip install piper-tts piper-phonemize-cross\n", " !pip install webrtcvad\n", " !pip install torch==2.5.0 torchvision==0.20.0 torchaudio==2.5.0 --index-url https://download.pytorch.org/whl/cu121\n", "\n", "# Modèle EN\n", "os.makedirs('piper-sample-generator/models', exist_ok=True)\n", "if not os.path.exists('piper-sample-generator/models/en_US-libritts_r-medium.pt'):\n", " !wget -q -O piper-sample-generator/models/en_US-libritts_r-medium.pt \\\n", " 'https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/en_US-libritts_r-medium.pt'\n", "\n", "# Modèle FR via piper-tts .onnx\n", "if not os.path.exists('fr_FR-siwis-medium.onnx'):\n", " !wget -q -O fr_FR-siwis-medium.onnx \\\n", " 'https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx'\n", " !wget -q -O fr_FR-siwis-medium.onnx.json \\\n", " 'https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx.json'\n", "\n", "if 'piper-sample-generator/' not in sys.path:\n", " sys.path.append('piper-sample-generator/')\n", "from generate_samples import generate_samples\n", "\n", "print('Piper prêt.')" ], "outputs": [], "execution_count": null }, { "cell_type": "code", "metadata": {}, "source": [ "# Test audio — vérifie les prononciations avant de lancer le training\n", "generate_samples(text=PHRASE_DEFAULT, max_samples=1, output_dir='./', batch_size=1,\n", " auto_reduce_batch_size=True, file_names=['test_en.wav'])\n", "print(f'EN : \"{PHRASE_DEFAULT}\"')\n", "display(Audio('test_en.wav', autoplay=False))\n", "\n", "!echo \"{PHRASE_FR}\" | piper --model fr_FR-siwis-medium.onnx --output_file test_fr.wav\n", "print(f'FR : \"{PHRASE_FR}\"')\n", "display(Audio('test_fr.wav', autoplay=False))" ], "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Étape 2 — Téléchargement des données" ] }, { "cell_type": "code", "metadata": {}, "source": [ "import locale\n", "def getpreferredencoding(do_setlocale=True):\n", " return 'UTF-8'\n", "locale.getpreferredencoding = getpreferredencoding\n", "\n", "import os, shutil\n", "\n", "# Clone openWakeWord\n", "if os.path.exists('./openwakeword'):\n", " shutil.rmtree('./openwakeword')\n", "!rm -rf ./openwakeword\n", "!git clone https://github.com/dscripka/openwakeword\n", "!pip install -e ./openwakeword --no-deps\n", "\n", "!pip install mutagen==1.47.0\n", "!pip install torchinfo==1.8.0\n", "!pip install torchmetrics==1.2.0\n", "!pip install speechbrain==0.5.14\n", "!pip install audiomentations==0.33.0\n", "!pip install torch-audiomentations==0.11.0\n", "!pip install acoustics==0.2.6\n", "!pip install onnxruntime==1.22.1 ai_edge_litert==1.4.0 onnxsim\n", "!pip install onnx2tf\n", "!pip install onnx==1.19.1\n", "!pip install onnx_graphsurgeon sng4onnx\n", "!pip install pronouncing==0.2.0\n", "!pip install datasets==2.14.6\n", "!pip install deep-phonemizer==0.0.19\n", "\n", "os.makedirs('./openwakeword/openwakeword/resources/models', exist_ok=True)\n", "base = 'https://github.com/dscripka/openWakeWord/releases/download/v0.5.1'\n", "for f in ['embedding_model.onnx', 'embedding_model.tflite', 'melspectrogram.onnx', 'melspectrogram.tflite']:\n", " !wget -q {base}/{f} -O ./openwakeword/openwakeword/resources/models/{f}\n", "\n", "print('Installation terminée.')" ], "outputs": [], "execution_count": null }, { "cell_type": "code", "metadata": {}, "source": [ "import numpy as np, sys, yaml, datasets, scipy\n", "from pathlib import Path\n", "from tqdm import tqdm\n", "\n", "# MIT Room Impulse Responses\n", "output_dir = './mit_rirs'\n", "if not os.path.exists(output_dir):\n", " os.mkdir(output_dir)\n", " !git lfs install\n", " !git clone https://huggingface.co/datasets/davidscripka/MIT_environmental_impulse_responses\n", " rir_dataset = datasets.Dataset.from_dict({\n", " 'audio': [str(i) for i in Path('./MIT_environmental_impulse_responses/16khz').glob('*.wav')]\n", " }).cast_column('audio', datasets.Audio())\n", " for row in tqdm(rir_dataset):\n", " name = row['audio']['path'].split('/')[-1]\n", " scipy.io.wavfile.write(os.path.join(output_dir, name), 16000, (row['audio']['array']*32767).astype(np.int16))\n", "\n", "# FMA music\n", "output_dir = './fma'\n", "if not os.path.exists(output_dir):\n", " os.mkdir(output_dir)\n", " fma_dataset = datasets.load_dataset('rudraml/fma', name='small', split='train', streaming=True)\n", " fma_dataset = iter(fma_dataset.cast_column('audio', datasets.Audio(sampling_rate=16000)))\n", " n_hours = 1\n", " for i in tqdm(range(n_hours*3600//30)):\n", " row = next(fma_dataset)\n", " name = row['audio']['path'].split('/')[-1].replace('.mp3', '.wav')\n", " scipy.io.wavfile.write(os.path.join(output_dir, name), 16000, (row['audio']['array']*32767).astype(np.int16))\n", "\n", "# Features pré-calculées openWakeWord\n", "if not os.path.exists('./openwakeword_features_ACAV100M_2000_hrs_16bit.npy'):\n", " !wget -q https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/openwakeword_features_ACAV100M_2000_hrs_16bit.npy\n", "if not os.path.exists('validation_set_features.npy'):\n", " !wget -q https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/validation_set_features.npy\n", "\n", "print('Données prêtes.')" ], "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Étape 3 — Génération des samples multi-langue" ] }, { "cell_type": "code", "metadata": {}, "source": "import sys, os, glob\nimport scipy.io.wavfile as wav\nimport scipy.signal as signal\nimport numpy as np\nfrom tqdm import tqdm\n\nif 'piper-sample-generator/' not in sys.path:\n sys.path.append('piper-sample-generator/')\nfrom generate_samples import generate_samples\n\nos.makedirs(CLIPS_DIR, exist_ok=True)\n\n# ── EN via piper-sample-generator ────────────────────────────────────────────\nen_dir = f'{CLIPS_DIR}/en'\nos.makedirs(en_dir, exist_ok=True)\nexisting_en = len([f for f in os.listdir(en_dir) if f.endswith('.wav')])\nremaining_en = lang_samples['en'] - existing_en\n\nif remaining_en > 0:\n print(f' [en] génération {remaining_en} samples : \"{PHRASE_DEFAULT}\"')\n generate_samples(\n text=PHRASE_DEFAULT, max_samples=remaining_en,\n length_scales=[0.85, 0.9, 0.95, 1.0, 1.05, 1.1, 1.15],\n noise_scales=[0.4, 0.667, 1.0], noise_scale_ws=[0.8],\n output_dir=en_dir, batch_size=50, auto_reduce_batch_size=True,\n )\nelse:\n print(f' [en] déjà complet ({existing_en}/{lang_samples[\"en\"]})')\n\n# ── FR via piper CLI + rééchantillonnage 16kHz ───────────────────────────────\nfr_dir = f'{CLIPS_DIR}/fr'\nos.makedirs(fr_dir, exist_ok=True)\nexisting_fr = len([f for f in os.listdir(fr_dir) if f.endswith('.wav')])\nremaining_fr = lang_samples['fr'] - existing_fr\n\nif remaining_fr > 0:\n print(f' [fr] génération {remaining_fr} samples : \"{PHRASE_FR}\"')\n speeds = [0.85, 0.9, 0.95, 1.0, 1.05, 1.1, 1.15]\n n_per_speed = remaining_fr // len(speeds)\n remainder = remaining_fr % len(speeds)\n for i, speed in enumerate(tqdm(speeds)):\n n = n_per_speed + (1 if i < remainder else 0)\n with open('/tmp/fr_input.txt', 'w', encoding='utf-8') as f:\n f.write('\\n'.join([PHRASE_FR] * n))\n !piper --model fr_FR-siwis-medium.onnx --length_scale {speed} --output_dir {fr_dir} < /tmp/fr_input.txt\n\n resampled = 0\n for path in glob.glob(f'{fr_dir}/*.wav'):\n if os.path.getsize(path) == 0:\n os.remove(path)\n continue\n try:\n sr, data = wav.read(path)\n if sr != 16000:\n n_s = round(len(data) * 16000 / sr)\n wav.write(path, 16000, signal.resample(data, n_s).astype(np.int16))\n resampled += 1\n except Exception:\n os.remove(path)\n print(f' [fr] {resampled} clips rééchantillonnés à 16kHz')\nelif lang_samples['fr'] > 0:\n print(f' [fr] déjà complet ({existing_fr}/{lang_samples[\"fr\"]})')\nelse:\n print(' [fr] désactivé (fr_pct = 0)')\n\ntotal = sum(\n len([f for f in os.listdir(f'{CLIPS_DIR}/{lang}') if f.endswith('.wav')])\n for lang in ['en', 'fr'] if os.path.exists(f'{CLIPS_DIR}/{lang}')\n)\nprint(f'\\nTotal clips générés : {total}/{number_of_examples}')", "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Étape 4 — Training" ] }, { "cell_type": "code", "metadata": {}, "source": "import yaml, os, sys, shutil, glob, random\nimport scipy.io.wavfile as wav\nimport scipy.signal as signal\nimport numpy as np\n\nconfig = yaml.load(open('openwakeword/examples/custom_model.yml', 'r').read(), yaml.Loader)\n\nPHRASE_DEFAULT_SPACE = PHRASE_DEFAULT.replace('_', ' ')\n\nconfig['target_phrase'] = [PHRASE_DEFAULT_SPACE, PHRASE_FR]\nconfig['model_name'] = MODEL_NAME\nconfig['n_samples'] = number_of_examples\nconfig['n_samples_val'] = max(500, number_of_examples // 10)\nconfig['steps'] = number_of_training_steps\nconfig['target_accuracy'] = target_accuracy\nconfig['target_recall'] = target_recall\nconfig['max_negative_weight'] = false_activation_penalty\n\nfinal_drive_output_dir = os.path.join(drive_output_dir, MODEL_NAME)\nos.makedirs(final_drive_output_dir, exist_ok=True)\n\nlocal_temp_dir = './openwakeword_training_temp'\nconfig['output_dir'] = local_temp_dir\nconfig['background_paths'] = ['./fma']\nconfig['false_positive_validation_data_path'] = 'validation_set_features.npy'\nconfig['feature_data_files'] = {'ACAV100M_sample': 'openwakeword_features_ACAV100M_2000_hrs_16bit.npy'}\n\nwith open('my_model.yaml', 'w') as f:\n yaml.dump(config, f)\n\n# Rééchantillonner les clips FR à 16kHz\nfor path in glob.glob(f'{CLIPS_DIR}/fr/*.wav'):\n if os.path.getsize(path) == 0:\n os.remove(path)\n continue\n try:\n sr, data = wav.read(path)\n if sr != 16000:\n n_s = round(len(data) * 16000 / sr)\n wav.write(path, 16000, signal.resample(data, n_s).astype(np.int16))\n except Exception:\n os.remove(path)\n\n# Setup des dossiers via --generate_clips\nprint('Setup des dossiers via --generate_clips...')\n!{sys.executable} openwakeword/openwakeword/train.py --training_config my_model.yaml --generate_clips\n\n# S'assurer que les dossiers existent\npositive_clips_dir = os.path.join(local_temp_dir, MODEL_NAME, 'positive_clips')\npositive_test_dir = os.path.join(local_temp_dir, MODEL_NAME, 'positive_test')\nos.makedirs(positive_clips_dir, exist_ok=True)\nos.makedirs(positive_test_dir, exist_ok=True)\n\n# Remplacer les clips EN par nos clips multi-langue\nfor f in glob.glob(os.path.join(positive_clips_dir, '*.wav')): os.remove(f)\nfor f in glob.glob(os.path.join(positive_test_dir, '*.wav')): os.remove(f)\n\nall_clips = glob.glob(f'{CLIPS_DIR}/**/*.wav', recursive=True)\nrandom.shuffle(all_clips)\nn_val = config['n_samples_val']\nfor wav_path in all_clips[:n_val]:\n lang = os.path.basename(os.path.dirname(wav_path))\n shutil.copy(wav_path, os.path.join(positive_test_dir, f\"{lang}_{os.path.basename(wav_path)}\"))\nfor wav_path in all_clips[n_val:]:\n lang = os.path.basename(os.path.dirname(wav_path))\n shutil.copy(wav_path, os.path.join(positive_clips_dir, f\"{lang}_{os.path.basename(wav_path)}\"))\n\nprint(f'Training : {len(all_clips) - n_val} clips | Validation : {n_val} clips')\n\n# Supprimer le cache .npy\nfor f in glob.glob(os.path.join(local_temp_dir, MODEL_NAME, '*.npy')):\n os.remove(f)\n print(f'Cache supprimé : {os.path.basename(f)}')\n\n# Augmentation + training\n!{sys.executable} openwakeword/openwakeword/train.py --training_config my_model.yaml --augment_clips\n!{sys.executable} openwakeword/openwakeword/train.py --training_config my_model.yaml --train_model", "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Étape 5 — Export ONNX → TFLite + sauvegarde Drive" ] }, { "cell_type": "code", "metadata": {}, "source": [ "import shutil, os\n", "from google.colab import files\n", "\n", "local_onnx = f'{local_temp_dir}/{MODEL_NAME}.onnx'\n", "drive_onnx = f'{final_drive_output_dir}/{MODEL_NAME}.onnx'\n", "drive_tflite1 = f'{final_drive_output_dir}/{MODEL_NAME}_float32.tflite'\n", "drive_tflite = f'{final_drive_output_dir}/{MODEL_NAME}.tflite'\n", "\n", "shutil.copy(local_onnx, drive_onnx)\n", "print(f'ONNX copié : {drive_onnx}')\n", "\n", "!onnx2tf -i \"{drive_onnx}\" -o \"{final_drive_output_dir}\" -kat onnx____Flatten_0\n", "!mv \"{drive_tflite1}\" \"{drive_tflite}\"\n", "print(f'TFLite : {drive_tflite}')\n", "\n", "files.download(drive_onnx)\n", "files.download(drive_tflite)" ], "outputs": [], "execution_count": null } ] }