mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 16:07:10 +00:00
V1.010 préparation de la création d'une banque de dictées
This commit is contained in:
+295
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* ASPIC — micro_test.js
|
||||
*
|
||||
* Outil de mise au point interne (3 septembre 2026, demande explicite de
|
||||
* Jean-Marc) : PAS un futur module utilisateur. Sert uniquement à
|
||||
* constituer une banque de dictées réelles (table ASP_DicteesTest, thème
|
||||
* "Gestion des Contacts") pour préparer, plus tard, la conception des
|
||||
* règles d'interprétation du pilotage vocal — voir réflexions
|
||||
* stratégiques et le commentaire d'en-tête de dictees_test.php.
|
||||
*
|
||||
* Chargé À LA PLACE de micro.js sur ecran_fond.html, le temps de cette
|
||||
* phase de mise au point (voir le commentaire dédié dans ecran_fond.html,
|
||||
* autour de #micro-desk) — micro.js (le vrai pilotage : agents nommés,
|
||||
* mot déclencheur, mode notation) reste totalement inchangé et continue de
|
||||
* tourner normalement sur liste_contacts.html. Pour reconnecter le vrai
|
||||
* pilotage sur le micro de l'écran de fond une fois cette phase terminée :
|
||||
* remettre <script src="micro.js"> à la place de
|
||||
* <script src="micro_test.js"> en bas d'ecran_fond.html, rien d'autre à
|
||||
* défaire — ce fichier-ci n'est jamais modifié pour ça.
|
||||
*
|
||||
* Différences volontaires avec micro.js, toutes actées avec Jean-Marc :
|
||||
* - Aucun mot déclencheur : le clic sur le micro fait déjà office de
|
||||
* signal explicite de début (contrairement à l'écoute ambiante continue
|
||||
* de micro.js, qui doit filtrer une conversation de fond d'une vraie
|
||||
* commande — ce problème ne se pose pas ici, on sait que tout ce qui est
|
||||
* dit après le clic est un essai volontaire).
|
||||
* - Aucune notion d'agent, de mode notation, de phrase de clôture : chaque
|
||||
* énoncé séparé par un silence est une ligne indépendante, enregistrée
|
||||
* immédiatement dès qu'il est reconnu — la segmentation elle-même n'est
|
||||
* pas gérée à la main ici, c'est le moteur de reconnaissance du
|
||||
* navigateur qui la fait (un événement "result" avec isFinal=true
|
||||
* correspond déjà à "ce qui a été dit avant une pause" ; on ne choisit
|
||||
* pas la durée exacte de cette pause, elle est interne au navigateur —
|
||||
* même mécanisme que celui déjà éprouvé en production dans micro.js).
|
||||
* - Restitution au fur et à mesure dans une fenêtre dédiée (fond noir,
|
||||
* texte blanc, défilement) : la ligne en cours s'affiche pendant la
|
||||
* dictée (interimResults activé, contrairement à micro.js) puis se fige
|
||||
* et se sauvegarde dès que le silence est détecté par le navigateur ;
|
||||
* une nouvelle ligne en cours démarre aussitôt après.
|
||||
* - Aucune suppression possible depuis l'écran, y compris pour un essai
|
||||
* raté (décision explicite de Jean-Marc, 3 septembre 2026) : un post-it
|
||||
* raté fera aussi partie de la vraie vie des futurs utilisateurs, la
|
||||
* banque de tests doit refléter ça dès maintenant plutôt que ne garder
|
||||
* que les essais réussis.
|
||||
* - Fenêtre ouverte à chaque ouverture du micro, vidée à chaque fermeture
|
||||
* (elle ne conserve donc rien visuellement d'une session à l'autre — la
|
||||
* table ASP_DicteesTest, elle, continue évidemment d'accumuler tous les
|
||||
* essais, la fenêtre n'est qu'un retour visuel immédiat).
|
||||
*
|
||||
* Reprend, pour la robustesse, le même squelette que micro.js pour le
|
||||
* redémarrage silencieux du moteur (qui peut s'arrêter tout seul sans que
|
||||
* l'utilisateur l'ait demandé) et la coupure automatique après inactivité
|
||||
* prolongée (même prudence de confidentialité que celle qui avait conduit
|
||||
* à cet ajout dans micro.js — voir son commentaire d'en-tête) — sans
|
||||
* dépendre de micro.js lui-même : ce fichier est volontairement autonome et
|
||||
* plus simple, pour ne jamais risquer de contaminer le vrai pilotage avec
|
||||
* de la logique de test jetable.
|
||||
*/
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
const bouton = document.getElementById("micro-desk");
|
||||
if(!bouton) return; // ce script ne concerne que l'écran de fond
|
||||
|
||||
const ReconnaissanceVocale = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
if(!ReconnaissanceVocale){
|
||||
bouton.title = "Reconnaissance vocale non disponible sur ce navigateur";
|
||||
return;
|
||||
}
|
||||
|
||||
const reconnaissance = new ReconnaissanceVocale();
|
||||
reconnaissance.lang = "fr-FR";
|
||||
reconnaissance.continuous = true;
|
||||
reconnaissance.interimResults = true; // restitution au fur et à mesure, demandée par Jean-Marc
|
||||
reconnaissance.maxAlternatives = 1;
|
||||
|
||||
let ecouteVoulue = false;
|
||||
let erreurBloquante = false;
|
||||
|
||||
// Coupure automatique après inactivité — même prudence que micro.js
|
||||
// (voir son commentaire d'en-tête sur l'incident de confidentialité qui
|
||||
// avait motivé cet ajout) : un outil de test resté ouvert par oubli ne
|
||||
// doit pas non plus continuer à envoyer l'audio ambiant indéfiniment.
|
||||
const DUREE_INACTIVITE_MS = 10 * 60 * 1000; // 10 minutes
|
||||
let minuteurInactivite = null;
|
||||
|
||||
function reinitialiserMinuteurInactivite(){
|
||||
if(minuteurInactivite) clearTimeout(minuteurInactivite);
|
||||
minuteurInactivite = setTimeout(couperPourInactivite, DUREE_INACTIVITE_MS);
|
||||
}
|
||||
|
||||
function arreterMinuteurInactivite(){
|
||||
if(minuteurInactivite){
|
||||
clearTimeout(minuteurInactivite);
|
||||
minuteurInactivite = null;
|
||||
}
|
||||
}
|
||||
|
||||
function couperPourInactivite(){
|
||||
minuteurInactivite = null;
|
||||
if(!ecouteVoulue) return; // déjà refermé entre-temps
|
||||
ecouteVoulue = false;
|
||||
reconnaissance.stop();
|
||||
}
|
||||
|
||||
// -------- Fenêtre de restitution --------
|
||||
// Construite ici plutôt que dans ecran_fond.html, pour que ce fichier
|
||||
// reste entièrement autonome — le retirer (remettre micro.js) ne laisse
|
||||
// aucune trace HTML/CSS à nettoyer côté page hôte.
|
||||
let fenetre = null;
|
||||
let zoneLignes = null;
|
||||
let ligneEnCours = null;
|
||||
|
||||
function construireFenetre(){
|
||||
if(fenetre) return;
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent =
|
||||
"#fenetre-dictees-test{position:fixed;top:8%;left:50%;" +
|
||||
"transform:translateX(-50%);width:min(560px,80vw);height:min(420px,70vh);" +
|
||||
"background:#000;border:1px solid #333;border-radius:6px;" +
|
||||
"box-shadow:0 10px 40px rgba(0,0,0,.6);z-index:950;display:none;" +
|
||||
"flex-direction:column;overflow:hidden;" +
|
||||
"font-family:'Courier New',Consolas,monospace;}" +
|
||||
"#fenetre-dictees-test.visible{display:flex;}" +
|
||||
"#fenetre-dictees-test .entete{color:#888;font-size:11px;" +
|
||||
"padding:6px 10px;border-bottom:1px solid #222;flex:none;}" +
|
||||
"#fenetre-dictees-test .lignes{flex:1;overflow-y:auto;padding:8px 10px;" +
|
||||
"color:#fff;font-size:14px;line-height:1.6;white-space:pre-wrap;}" +
|
||||
"#fenetre-dictees-test .lignes .en-cours{color:#8a8a8a;font-style:italic;}" +
|
||||
"#fenetre-dictees-test .lignes .erreur{color:#e14a5c;}";
|
||||
document.head.appendChild(style);
|
||||
|
||||
fenetre = document.createElement("div");
|
||||
fenetre.id = "fenetre-dictees-test";
|
||||
fenetre.innerHTML =
|
||||
'<div class="entete">Banque de dictées — Gestion des Contacts ' +
|
||||
'(outil de mise au point, non destiné aux utilisateurs finaux)</div>' +
|
||||
'<div class="lignes"></div>';
|
||||
document.body.appendChild(fenetre);
|
||||
zoneLignes = fenetre.querySelector(".lignes");
|
||||
}
|
||||
|
||||
function ouvrirFenetre(){
|
||||
construireFenetre();
|
||||
zoneLignes.innerHTML = "";
|
||||
ligneEnCours = null;
|
||||
fenetre.classList.add("visible");
|
||||
}
|
||||
|
||||
function fermerFenetre(){
|
||||
if(fenetre) fenetre.classList.remove("visible");
|
||||
if(zoneLignes) zoneLignes.innerHTML = "";
|
||||
ligneEnCours = null;
|
||||
}
|
||||
|
||||
function afficherEnCours(texte){
|
||||
if(!zoneLignes) return;
|
||||
if(!ligneEnCours){
|
||||
ligneEnCours = document.createElement("div");
|
||||
ligneEnCours.className = "en-cours";
|
||||
zoneLignes.appendChild(ligneEnCours);
|
||||
}
|
||||
ligneEnCours.textContent = texte;
|
||||
zoneLignes.scrollTop = zoneLignes.scrollHeight;
|
||||
}
|
||||
|
||||
function figerLigne(texte){
|
||||
if(!zoneLignes) return;
|
||||
if(!ligneEnCours){
|
||||
ligneEnCours = document.createElement("div");
|
||||
zoneLignes.appendChild(ligneEnCours);
|
||||
}
|
||||
ligneEnCours.className = "";
|
||||
ligneEnCours.textContent = texte;
|
||||
ligneEnCours = null; // la prochaine ligne (en cours ou figée) en recrée une
|
||||
zoneLignes.scrollTop = zoneLignes.scrollHeight;
|
||||
}
|
||||
|
||||
function afficherErreur(texte){
|
||||
if(!zoneLignes) return;
|
||||
const div = document.createElement("div");
|
||||
div.className = "erreur";
|
||||
div.textContent = texte;
|
||||
zoneLignes.appendChild(div);
|
||||
zoneLignes.scrollTop = zoneLignes.scrollHeight;
|
||||
}
|
||||
|
||||
// -------- Enregistrement --------
|
||||
// Aucune interprétation, aucun filtrage : tout ce qui est capté part vers
|
||||
// dictees_test.php, essai raté compris (décision de Jean-Marc, voir
|
||||
// commentaire d'en-tête).
|
||||
function enregistrerDictee(texte){
|
||||
fetch("dictees_test.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({texte: texte})
|
||||
}).then(function(reponse){
|
||||
if(!reponse.ok) throw new Error("HTTP " + reponse.status);
|
||||
}).catch(function(){
|
||||
afficherErreur("(non enregistré — erreur de connexion à dictees_test.php)");
|
||||
});
|
||||
}
|
||||
|
||||
function passerEnEcoute(){
|
||||
bouton.classList.add("ecoute");
|
||||
bouton.title = "Le micro est ouvert (banque de dictées contacts — outil de mise au point)";
|
||||
ouvrirFenetre();
|
||||
reinitialiserMinuteurInactivite();
|
||||
}
|
||||
|
||||
function revenirAuRepos(){
|
||||
ecouteVoulue = false;
|
||||
arreterMinuteurInactivite();
|
||||
bouton.classList.remove("ecoute");
|
||||
bouton.title = "Le micro est inactif";
|
||||
fermerFenetre();
|
||||
}
|
||||
|
||||
bouton.addEventListener("click", function(){
|
||||
if(ecouteVoulue){
|
||||
// Fermeture demandée : le retour au repos (et la fermeture de la
|
||||
// fenêtre) se fait via l'événement "end" qui va suivre.
|
||||
ecouteVoulue = false;
|
||||
reconnaissance.stop();
|
||||
return;
|
||||
}
|
||||
ecouteVoulue = true;
|
||||
erreurBloquante = false;
|
||||
try{
|
||||
reconnaissance.start();
|
||||
}catch(erreur){
|
||||
ecouteVoulue = false;
|
||||
afficherErreur("Impossible de démarrer le micro (" + erreur.name + ").");
|
||||
}
|
||||
});
|
||||
|
||||
reconnaissance.addEventListener("start", passerEnEcoute);
|
||||
|
||||
reconnaissance.addEventListener("end", function(){
|
||||
if(ecouteVoulue && !erreurBloquante){
|
||||
// Le moteur s'est arrêté tout seul (silence prolongé, limite interne
|
||||
// du navigateur...) alors que l'écoute reste voulue : relance
|
||||
// invisible pour l'utilisateur, même principe que micro.js.
|
||||
try{
|
||||
reconnaissance.start();
|
||||
}catch(erreur){
|
||||
revenirAuRepos();
|
||||
afficherErreur("Le micro s'est arrêté et n'a pas pu redémarrer automatiquement.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
revenirAuRepos();
|
||||
});
|
||||
|
||||
reconnaissance.addEventListener("result", function(evenement){
|
||||
const dernier = evenement.results[evenement.results.length - 1];
|
||||
const texte = dernier[0].transcript.trim();
|
||||
if(!texte) return;
|
||||
|
||||
if(dernier.isFinal){
|
||||
// Silence détecté par le moteur : la ligne se fige et s'enregistre
|
||||
// immédiatement, essai raté ou non — aucun jugement porté ici sur la
|
||||
// qualité du texte reconnu.
|
||||
figerLigne(texte);
|
||||
enregistrerDictee(texte);
|
||||
reinitialiserMinuteurInactivite();
|
||||
} else {
|
||||
// Restitution au fur et à mesure, avant que le silence ne fige la
|
||||
// ligne — demande explicite de Jean-Marc.
|
||||
afficherEnCours(texte);
|
||||
}
|
||||
});
|
||||
|
||||
reconnaissance.addEventListener("error", function(evenement){
|
||||
const messagesBloquants = {
|
||||
"not-allowed": "Micro refusé — vérifie l'autorisation dans le navigateur.",
|
||||
"service-not-allowed": "Micro refusé — vérifie l'autorisation dans le navigateur.",
|
||||
"audio-capture": "Aucun micro détecté."
|
||||
};
|
||||
if(messagesBloquants[evenement.error]){
|
||||
erreurBloquante = true;
|
||||
afficherErreur(messagesBloquants[evenement.error]);
|
||||
return;
|
||||
}
|
||||
// "no-speech" est un bruit normal entre deux essais (silence pendant
|
||||
// qu'on réfléchit à la phrase suivante) : pas affiché, pour ne pas
|
||||
// polluer la fenêtre de tests — l'événement "end" qui suit relance de
|
||||
// toute façon l'écoute tant qu'elle reste voulue.
|
||||
if(evenement.error === "network"){
|
||||
afficherErreur("(problème réseau, nouvelle tentative...)");
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user