/** * ASPIC — micro.js * * Comportement du bouton micro (#bouton-micro), partagé par tous les * écrans qui l'affichent — chargé via en * toute fin de page, après le script propre à chaque écran. * * Pourquoi un fichier séparé plutôt que dupliqué dans le gabarit comme le * badge société ou l'avatar : ceux-là ne sont que de la présentation, alors * qu'ici il s'agit de comportement (reconnaissance vocale, puis * interprétation de commandes) qui doit rester rigoureusement identique * partout, sur un nombre d'écrans appelé à grandir — une correction ne * doit se faire qu'à un seul endroit. Nommé "micro.js" plutôt que "voix.js" * car appelé à porter d'autres fonctionnalités liées à la voix au fil des * prochains chantiers, pas seulement la reconnaissance. * * Chaque page hôte doit déjà contenir la structure suivante dans son * bandeau (voir liste_contacts.html / liste_todolist.html) : * #bouton-micro > #icone-micro (svg, icône micro barré) + #barres-ecoute * et peut définir une fonction globale afficherToast(message) pour le * retour visuel — micro.js s'en sert si elle existe, sinon se contente * d'un console.log (utile en test avant d'avoir une page complète). * * Étape 1 (1er septembre 2026) : reconnaissance vocale réelle branchée sur * le bouton, texte transcrit simplement affiché en retour pour valider le * circuit micro -> texte. Aucune commande interprétée ni action * déclenchée à ce stade — ce sera l'étape 2 (ex. ajout de tâche à la * ToDoList), une fois ces premiers tests concluants. Le stockage des * textes entendus dans une table dédiée, évoqué par Jean-Marc pour la * suite, n'est pas non plus branché ici — à ajouter une fois la structure * de table définie avec lui. * * Mode utilisé : reconnaissance "cloud" du navigateur (SpeechRecognition / * webkitSpeechRecognition) — mature et gratuite, au prix d'un envoi de * l'audio à un service externe (voir réflexions stratégiques, section 3). * Bascule vers le mode local ("on-device", encore expérimental côté Edge à * ce jour) envisageable plus tard sans changer la structure de ce fichier. * * Mot déclencheur "Lilou" (1er septembre 2026) : en écoute continue, * chaque phrase captée est transcrite — y compris une conversation de fond * non destinée à ASPIC. Plutôt que de réagir à tout, seules les phrases * commençant par "Lilou" (le prénom donné à l'assistante vocale d'ASPIC, * clin d'œil au Cinquième Élément — Viktor s'occupant déjà des contacts * côté commercial) sont prises en compte ; le reste est ignoré en * silence. Nuance à garder en tête : ce filtre s'applique sur le texte * déjà transcrit, pas avant l'envoi au service de reconnaissance — le son * continue d'être transmis tant que le micro est ouvert, qu'on y trouve * "Lilou" ou non (voir réflexions stratégiques pour la discussion sur le * mode local, seule vraie façon d'éviter cet envoi). * * Mode "notation" — capture d'une note sans interprétation (1er septembre * 2026, demande explicite de Jean-Marc) : une phrase d'ouverture ("Lilou, * ajoute une tâche...", "Lilou, un truc à faire...", "Lilou, note ça dans * ma todolist...") fait basculer dans un mode où le "Lilou" en tête n'est * plus exigé pour chaque ligne suivante — tout ce qui est capté est ajouté * tel quel au fichier, jusqu'à une phrase de clôture ("c'est tout Lilou") * ou d'annulation ("Lilou annule"), qui produisent exactement le même * comportement (on enregistre ce qui a été dit dans les deux cas — pas de * traitement différencié pour l'annulation, sur demande explicite de * Jean-Marc : lui analysera ses propres essais après coup). "ensuite" en * fin de phrase sert de marqueur oral de fin de ligne, retiré du texte * conservé. Volontairement SANS interprétation à ce stade — le texte part * tel quel dans notes_lilou/dictees.txt (via lilou_notes.php) pour * constituer un corpus de vrais essais, avant de définir des règles * d'interprétation dans un prochain chantier (étape 2, envisagée via n8n). * Les phrases déclenchantes ci-dessous sont volontairement en dur et * simples (pas de grammaire compliquée) — à ajuster une fois les premiers * essais de Jean-Marc faits. * * Écoute "tant qu'on ne l'a pas coupée soi-même" (1er septembre 2026, * ajusté après un premier essai de Jean-Marc) : `continuous = true` ne * suffit pas à lui seul — de façon documentée (y compris sur Chrome * desktop, pas seulement mobile), le moteur du navigateur peut mettre fin * à l'écoute de lui-même après un silence prolongé ou au bout d'un certain * temps, sans que ce soit une erreur ni une action de l'utilisateur. On * distingue donc "l'utilisateur veut que le micro soit ouvert" * (ecouteVoulue) de l'état réel du moteur : si celui-ci s'arrête tout seul * alors que l'écoute est toujours voulue, on le relance aussitôt — de façon * invisible pour l'utilisateur, qui n'a donc à cliquer qu'une fois pour * ouvrir le micro et une fois pour le refermer. Exception : une erreur * bloquante (micro refusé, pas de micro détecté) ne redémarre pas * automatiquement, pour ne pas boucler sans fin sur un blocage qui ne se * résoudra pas tout seul. */ (function(){ "use strict"; const bouton = document.getElementById("bouton-micro"); if(!bouton) return; // écran sans bouton micro dans son bandeau function notifier(message){ if(typeof window.afficherToast === "function"){ window.afficherToast(message); } else { console.log("[micro.js]", message); } } const ReconnaissanceVocale = window.SpeechRecognition || window.webkitSpeechRecognition; if(!ReconnaissanceVocale){ // Navigateur sans support de l'API : bouton laissé visible (cohérence // du bandeau) mais explicite sur l'indisponibilité au clic. bouton.title = "Reconnaissance vocale non disponible sur ce navigateur"; bouton.addEventListener("click", function(){ notifier("La reconnaissance vocale n'est pas disponible sur ce navigateur."); }); return; } const reconnaissance = new ReconnaissanceVocale(); reconnaissance.lang = "fr-FR"; reconnaissance.continuous = true; reconnaissance.interimResults = false; reconnaissance.maxAlternatives = 1; // ecouteVoulue : ce que l'utilisateur a demandé (vrai entre un clic // d'ouverture et le clic de fermeture, quels que soient les redémarrages // silencieux du moteur entre les deux). let ecouteVoulue = false; // erreurBloquante : mise à vrai par une erreur qui ne se résoudra pas en // relançant tout de suite (micro refusé, pas de micro détecté) — empêche // le redémarrage automatique sur l'événement "end" qui suit. let erreurBloquante = false; function passerEnEcoute(){ bouton.classList.add("ecoute"); bouton.title = "Le micro est ouvert"; } function revenirAuRepos(){ ecouteVoulue = false; bouton.classList.remove("ecoute"); bouton.title = "Le micro est inactif"; } bouton.addEventListener("click", function(){ if(ecouteVoulue){ // Fermeture demandée par l'utilisateur : le retour au repos se fait // via l'événement "end" qui va suivre, sans redémarrage puisque // ecouteVoulue passe à faux avant l'appel à stop(). ecouteVoulue = false; reconnaissance.stop(); return; } ecouteVoulue = true; erreurBloquante = false; try{ reconnaissance.start(); }catch(erreur){ // start() peut lever une exception synchrone : soit un cycle déjà en // cours côté navigateur (double-clic très rapide, sans conséquence), // soit un vrai blocage (page pas en contexte sécurisé, permission // micro refusée avant même le début d'écoute selon le navigateur). // Dans le doute on prévient plutôt que de rester silencieux : mieux // vaut un message superflu sur un double-clic qu'un bouton muet qui // ne fait jamais rien sans explication. ecouteVoulue = false; notifier("Impossible de démarrer le micro (" + erreur.name + ")."); } }); // -------- Panneau de notation (affichage seul, construit par ce script // -------- pour être appelable depuis n'importe quel écran sans avoir à // -------- modifier chaque page hôte — voir commentaire d'en-tête. let panneauNotation = null; let listeNotation = null; function construirePanneauNotation(){ if(panneauNotation) return; const style = document.createElement("style"); style.textContent = "#panneau-notation-lilou{position:fixed;bottom:20px;right:20px;" + "max-width:280px;background:#FFFFFF;border:1px solid rgba(0,0,0,0.15);" + "border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,0.25);" + "padding:12px 16px;z-index:900;font-size:13px;display:none;}" + "#panneau-notation-lilou.visible{display:block;}" + "#panneau-notation-lilou .titre{font-weight:700;" + "color:var(--rouge-micro,#D9364B);margin-bottom:8px;}" + "#panneau-notation-lilou .lignes{list-style:none;margin:0;padding:0;}" + "#panneau-notation-lilou .lignes li{padding:2px 0 2px 14px;color:#2a2a2a;}"; document.head.appendChild(style); panneauNotation = document.createElement("div"); panneauNotation.id = "panneau-notation-lilou"; panneauNotation.innerHTML = '