269 lines
17 KiB
Plaintext
269 lines
17 KiB
Plaintext
{
|
|
"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
|
|
}
|
|
]
|
|
} |