Dictée : Parakeet remplace whisper.cpp

Parakeet TDT 0.6B v3 (NVIDIA), servi par sherpa-onnx. La v3 et non la v2 :
c'est la seule des deux qui parle français — la v2 est anglais seul.

CE QUI DISPARAÎT DU DOCKERFILE
Deux étapes de compilation, dont une CUDA de ~200 fichiers nvcc qui a été tuée
par l'OOM du runner plus d'une fois, et avec elles tout l'appareillage de
garde-fous qu'elles réclamaient (GGML_NATIVE=OFF contre le SIGILL en
production, bornage des architectures CUDA, deux binaires CPU/CUDA à choisir à
l'exécution). À la place : le téléchargement d'un binaire statique de 35 Mo,
version épinglée et empreinte SHA-256 vérifiée — le binaire s'exécute sur la
machine de l'utilisateur, une release remplacée en amont ne doit pas passer en
silence.

CE QUI CHANGE DANS LE CODE
La forme est la même — un processus local supervisé, éteint après dix minutes
d'inactivité. Le dialogue, lui, change : whisper-server exposait du HTTP
multipart, sherpa-onnx n'expose qu'un WebSocket dont le protocole tient en deux
entiers et des flottants. D'où un client WebSocket et une conversion WAV →
float32 côté serveur. Le parcours des blocs du WAV n'est pas du zèle : l'offset
44 codé en dur transforme un bloc LIST intercalé en craquement au début de
chaque phrase.

DEUX RÉGLAGES DISPARAISSENT, ET C'EST LE MOTEUR QUI L'IMPOSE
La LANGUE : Parakeet la détecte lui-même, il n'a aucun drapeau pour la forcer.
Le réglage n'aurait servi qu'à mentir. À surveiller : whisper avait précisément
écarté la détection automatique parce qu'elle se trompait sur des tranches
courtes.
Le GPU : le build livré est le statique CPU. Annoncer un sélecteur de carte
sans pouvoir l'honorer serait pire que de ne rien annoncer — et un modèle de
0,6 B en int8 sur des tranches de quelques secondes n'en a pas besoin, la carte
reste au moteur de chat.

MIGRATION
Un réglage enregistré du temps de whisper retombe sur le défaut au lieu de
casser la dictée. Les modèles ggml de /data/whisper/ ne servent plus à rien
mais ne sont PAS effacés : ce sont des données que personne n'a demandé de
perdre. Ils sont à supprimer à la main.

Le paquet UI regénéré ici couvre aussi les sources du commit précédent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-09-07 22:34:57 +02:00
1 parent 17f8fa8f34
commit 54468b3336
17 files changed
+1807 -894

No files matched your search

+5 -4
View File
@@ -1,9 +1,10 @@
// ─── Dictée vocale ───────────────────────────────────────────────────────────
// Bouton micro de la carte de saisie : un clic enregistre, un second arrête et
// transcrit. La capture est encodée en WAV 16 kHz mono ICI, côté navigateur —
// whisper.cpp ne lit que du PCM, et encoder au bon format évite d'embarquer
// ffmpeg dans l'image. La transcription est locale (POST /api/transcribe →
// whisper-cli). Le texte s'ajoute au champ sans écraser ce qui s'y trouve.
// le moteur de transcription ne lit que du PCM, et encoder au bon format évite
// d'embarquer ffmpeg dans l'image. La transcription est locale (POST
// /api/transcribe → Parakeet côté serveur). Le texte s'ajoute au champ sans
// écraser ce qui s'y trouve.
//
// ⚠️ Le micro n'est autorisé par le NAVIGATEUR qu'en contexte sécurisé (HTTPS
// ou localhost) : servi en http://IP:8090, getUserMedia n'existe pas — on
@@ -67,7 +68,7 @@ async function dictateStop(){
}
// Ré-échantillonne le PCM capturé (44,1/48 kHz selon la machine) vers du WAV
// 16 kHz mono 16 bits — le format d'entrée de whisper.cpp. Décimation simple :
// 16 kHz mono 16 bits — le format d’entrée du moteur de dictée. Décimation simple :
// pour de la voix ré-échantillonnée VERS LE BAS, largement suffisant.
function encodeWav16k(chunks, fromRate){
const toRate = 16000;
+15 -26
View File
@@ -1,41 +1,33 @@
// ─── Réglages de la dictée ───────────────────────────────────────────────────
// Panneau Paramètres → Dictée : matériel, modèle, langue, réactivité, plus un
// test du micro. Ce sont des réglages de SERVEUR (le modèle chargé et le GPU
// occupé appartiennent à la machine), donc rien n'est gardé en localStorage :
// tout passe par /api/dictate/*.
// Panneau Paramètres → Dictée : modèle, réactivité, plus un test du micro. Ce
// sont des réglages de SERVEUR (le modèle chargé appartient à la machine), donc
// rien n'est gardé en localStorage : tout passe par /api/dictate/*.
//
// Ni langue ni matériel ici, et le moteur l'impose : Parakeet détecte la langue
// lui-même, et le binaire livré est le build CPU. Afficher des sélecteurs qu'on
// ne peut pas honorer serait pire que de ne rien afficher.
let DIC_CFG = null;
async function loadDictateSettings(){
const [cfg, modeles, gpus] = await Promise.all([
const [cfg, modeles] = await Promise.all([
jfetch('/api/dictate/config').then(r => r.json()).catch(()=>({})),
jfetch('/api/dictate/models').then(r => r.json()).catch(()=>[]),
jfetch('/api/vram').then(r => r.json()).catch(()=>[]),
]);
DIC_CFG = cfg || {};
// Matériel : « CPU seul » d'abord — c'est le choix qui marche partout, y
// compris sur une image bâtie sans CUDA.
const dev = document.getElementById('dic-device');
dev.innerHTML = '';
dev.appendChild(new Option('processeur seul', 'cpu'));
(gpus||[]).forEach((g, i) => {
const libre = Math.max(0, (g.total|0) - (g.used|0));
dev.appendChild(new Option(`GPU ${i} — ${g.name} (${libre} Mo libres sur ${g.total|0})`, String(i)));
});
dev.value = cfg.device || 'cpu';
const mod = document.getElementById('dic-model');
mod.innerHTML = '';
(modeles||[]).forEach(m => {
const mo = Math.round(m.octets / 1e6);
// Dire ce qui est déjà là : sans ça, choisir un modèle absent ressemble à
// un réglage instantané alors que c'est un téléchargement de 600 Mo.
// un réglage instantané alors que c'est un téléchargement de ~500 Mo.
// Et dire ce qu'il COMPREND : c'est le vrai critère de choix entre les deux
// (v3 multilingue, v2 anglais seul), pas leur taille, quasi identique.
const etat = m.present ? 'déjà téléchargé' : mo + ' Mo à télécharger';
mod.appendChild(new Option(`${m.nom} — ${etat}, ~${m.vram_mo} Mo de mémoire`, m.id));
mod.appendChild(new Option(`${m.nom} — ${m.langues} · ${etat}`, m.id));
});
mod.value = cfg.model || '';
document.getElementById('dic-lang').value = cfg.lang || 'fr';
document.getElementById('dic-react').value = cfg.reactivity || 'moyen';
refreshDictateState();
}
@@ -43,8 +35,6 @@ async function loadDictateSettings(){
async function dictateSave(){
const cfg = {
model: document.getElementById('dic-model').value,
lang: document.getElementById('dic-lang').value,
device: document.getElementById('dic-device').value,
reactivity: document.getElementById('dic-react').value,
};
const el = document.getElementById('dic-save-state');
@@ -95,10 +85,9 @@ async function refreshDictateState(){
const bouts = [];
bouts.push(j.actif ? `moteur allumé depuis ${j.depuis_s||0} s` : 'moteur éteint (il démarre au premier clic sur le micro)');
bouts.push(j.present ? `modèle ${j.modele} présent` : `modèle ${j.modele} ABSENT — à télécharger`);
// note : le serveur nous dit quand il n'a pas pu respecter le matériel
// demandé (image sans CUDA, par exemple). Le taire ferait croire que le GPU
// choisi est utilisé.
if(j.note) bouts.push('⚠ ' + j.note);
// Le dire explicitement : sans ça, on cherche un réglage de carte qui
// n'existe pas, ou on croit à une panne devant un GPU inactif.
if(j.device === 'cpu') bouts.push('sur le processeur');
if(j.dl && j.dl.running) bouts.push(`téléchargement ${j.dl.id} ${j.dl.pct||0} %`);
el.textContent = bouts.join(' · ');
}