mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 16:07:10 +00:00
V1.012 Début du traitement par IA des retranscriptions des dictées
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* ASPIC — contact_ia_test.js
|
||||
*
|
||||
* Logique de contact_ia_test.html — outil de mise au point interne
|
||||
* (4 septembre 2026), voir l'en-tête de contact_ia_interprete.php pour le
|
||||
* contexte et les choix d'architecture.
|
||||
*
|
||||
* Reprend, pour la partie micro/reconnaissance vocale, le squelette déjà
|
||||
* éprouvé de micro_test.js (redémarrage silencieux du moteur, coupe-circuit
|
||||
* anti-boucle, coupure après inactivité) — volontairement dupliqué plutôt
|
||||
* que factorisé : ce fichier doit pouvoir être abandonné/retravaillé sans
|
||||
* aucun risque de toucher à micro_test.js, qui continue d'alimenter la
|
||||
* banque de dictées brutes en production. Contrairement à micro_test.js,
|
||||
* pas de "mode adresse" ici : si un champ dicté (email, orthographe d'un nom
|
||||
* rare...) est mal capté, la correction se fait directement dans le champ
|
||||
* du formulaire affiché après interprétation — c'est précisément
|
||||
* l'échappatoire "passer en saisie" actée avec Jean-Marc le 4 septembre
|
||||
* (réflexions stratégiques, section 12) : chaque champ du formulaire EST
|
||||
* cette échappatoire, pas besoin d'un mécanisme dédié en plus.
|
||||
*/
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
const boutonMicro = document.getElementById("bouton-micro");
|
||||
const boutonInterpreter = document.getElementById("bouton-interpreter");
|
||||
const boutonReinitialiser = document.getElementById("bouton-reinitialiser");
|
||||
const etatMicro = document.getElementById("etat-micro");
|
||||
const zoneLignes = document.getElementById("zone-lignes");
|
||||
const zoneResultat = document.getElementById("resultat");
|
||||
const boutonCreer = document.getElementById("bouton-creer");
|
||||
const messageCreation = document.getElementById("message-creation");
|
||||
const selectStatut = document.getElementById("f-statut");
|
||||
const selectProfession = document.getElementById("f-profession");
|
||||
const blocNouvelleProfession = document.getElementById("bloc-nouvelle-profession");
|
||||
const champNouvelleProfession = document.getElementById("f-profession-nouvelle");
|
||||
|
||||
// -------- Reconnaissance vocale (squelette repris de micro_test.js) --------
|
||||
const ReconnaissanceVocale = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
let reconnaissance = null;
|
||||
let ecouteVoulue = false;
|
||||
let erreurBloquante = false;
|
||||
let ligneEnCours = null;
|
||||
let texteComplet = ""; // buffer de toute la dictée depuis le dernier "Réinitialiser"
|
||||
let derniereExtraction = null; // dernière réponse de contact_ia_interprete.php, gardée pour comparaison avec ce que Jean-Marc valide ensuite
|
||||
|
||||
const DUREE_INACTIVITE_MS = 10 * 60 * 1000; // 10 minutes, même prudence que micro_test.js
|
||||
let minuteurInactivite = null;
|
||||
function reinitialiserMinuteurInactivite(){
|
||||
if(minuteurInactivite) clearTimeout(minuteurInactivite);
|
||||
minuteurInactivite = setTimeout(function(){
|
||||
if(!ecouteVoulue) return;
|
||||
ecouteVoulue = false;
|
||||
reconnaissance.stop();
|
||||
}, DUREE_INACTIVITE_MS);
|
||||
}
|
||||
function arreterMinuteurInactivite(){
|
||||
if(minuteurInactivite){ clearTimeout(minuteurInactivite); minuteurInactivite = null; }
|
||||
}
|
||||
|
||||
function afficherErreur(texte){
|
||||
const div = document.createElement("div");
|
||||
div.className = "erreur";
|
||||
div.textContent = texte;
|
||||
zoneLignes.appendChild(div);
|
||||
zoneLignes.scrollTop = zoneLignes.scrollHeight;
|
||||
}
|
||||
|
||||
function afficherEnCours(texte){
|
||||
if(!ligneEnCours){
|
||||
ligneEnCours = document.createElement("div");
|
||||
ligneEnCours.className = "en-cours";
|
||||
zoneLignes.appendChild(ligneEnCours);
|
||||
}
|
||||
ligneEnCours.textContent = texte;
|
||||
zoneLignes.scrollTop = zoneLignes.scrollHeight;
|
||||
}
|
||||
|
||||
function figerLigne(texte){
|
||||
if(!ligneEnCours){
|
||||
ligneEnCours = document.createElement("div");
|
||||
zoneLignes.appendChild(ligneEnCours);
|
||||
}
|
||||
ligneEnCours.className = "";
|
||||
ligneEnCours.textContent = texte;
|
||||
ligneEnCours = null;
|
||||
zoneLignes.scrollTop = zoneLignes.scrollHeight;
|
||||
|
||||
texteComplet = (texteComplet + " " + texte).trim();
|
||||
boutonInterpreter.disabled = (texteComplet === "");
|
||||
}
|
||||
|
||||
if(!ReconnaissanceVocale){
|
||||
boutonMicro.disabled = true;
|
||||
boutonMicro.title = "Reconnaissance vocale non disponible sur ce navigateur";
|
||||
} else {
|
||||
reconnaissance = new ReconnaissanceVocale();
|
||||
reconnaissance.lang = "fr-FR";
|
||||
reconnaissance.continuous = true;
|
||||
reconnaissance.interimResults = true;
|
||||
reconnaissance.maxAlternatives = 1;
|
||||
|
||||
let dernierDemarrageHorodatage = 0;
|
||||
let cyclesRapidesConsecutifs = 0;
|
||||
const SEUIL_CYCLE_RAPIDE_MS = 800;
|
||||
const MAX_CYCLES_RAPIDES = 5;
|
||||
|
||||
function passerEnEcoute(){
|
||||
boutonMicro.classList.add("actif");
|
||||
boutonMicro.textContent = "🎤 Écoute en cours…";
|
||||
etatMicro.textContent = "micro ouvert";
|
||||
reinitialiserMinuteurInactivite();
|
||||
}
|
||||
|
||||
function revenirAuRepos(){
|
||||
ecouteVoulue = false;
|
||||
arreterMinuteurInactivite();
|
||||
boutonMicro.classList.remove("actif");
|
||||
boutonMicro.textContent = "🎤 Micro";
|
||||
etatMicro.textContent = "";
|
||||
}
|
||||
|
||||
boutonMicro.addEventListener("click", function(){
|
||||
if(ecouteVoulue){
|
||||
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", function(){
|
||||
dernierDemarrageHorodatage = Date.now();
|
||||
passerEnEcoute();
|
||||
});
|
||||
|
||||
reconnaissance.addEventListener("end", function(){
|
||||
if(ecouteVoulue && !erreurBloquante){
|
||||
const ecoule = Date.now() - dernierDemarrageHorodatage;
|
||||
cyclesRapidesConsecutifs = (ecoule < SEUIL_CYCLE_RAPIDE_MS) ? (cyclesRapidesConsecutifs + 1) : 0;
|
||||
if(cyclesRapidesConsecutifs >= MAX_CYCLES_RAPIDES){
|
||||
erreurBloquante = true;
|
||||
revenirAuRepos();
|
||||
afficherErreur("Le micro redémarre en boucle anormalement vite — arrêt automatique. Réessaie dans quelques secondes.");
|
||||
return;
|
||||
}
|
||||
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){
|
||||
figerLigne(texte);
|
||||
reinitialiserMinuteurInactivite();
|
||||
} else {
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -------- Réinitialiser --------
|
||||
boutonReinitialiser.addEventListener("click", function(){
|
||||
if(ecouteVoulue && reconnaissance){ ecouteVoulue = false; reconnaissance.stop(); }
|
||||
texteComplet = "";
|
||||
ligneEnCours = null;
|
||||
zoneLignes.innerHTML = "";
|
||||
boutonInterpreter.disabled = true;
|
||||
zoneResultat.classList.remove("visible");
|
||||
messageCreation.textContent = "";
|
||||
messageCreation.className = "";
|
||||
});
|
||||
|
||||
// -------- Interpréter --------
|
||||
function viderFormulaire(){
|
||||
document.querySelectorAll("[data-champ]").forEach(function(el){ el.value = ""; el.classList.remove("interprete"); });
|
||||
selectStatut.innerHTML = '<option value="">—</option>';
|
||||
selectProfession.innerHTML = '<option value="">—</option><option value="__nouvelle__">+ Nouvelle profession…</option>';
|
||||
blocNouvelleProfession.style.display = "none";
|
||||
champNouvelleProfession.value = "";
|
||||
}
|
||||
|
||||
function remplirChampsTexte(champs){
|
||||
document.querySelectorAll("[data-champ]").forEach(function(el){
|
||||
const nomChamp = el.dataset.champ;
|
||||
if(nomChamp === "CNT_Statut") return; // géré à part (select)
|
||||
const valeur = champs[nomChamp];
|
||||
if(valeur){
|
||||
el.value = valeur;
|
||||
el.classList.add("interprete");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function remplirStatuts(statuts, statutChoisi){
|
||||
statuts.forEach(function(s){
|
||||
const option = document.createElement("option");
|
||||
option.value = s.STA_Code;
|
||||
option.textContent = s.STA_Libellé;
|
||||
selectStatut.appendChild(option);
|
||||
});
|
||||
if(statutChoisi){
|
||||
selectStatut.value = statutChoisi;
|
||||
selectStatut.classList.add("interprete");
|
||||
}
|
||||
}
|
||||
|
||||
function remplirProfessions(professions, idExistant, libelleNouveau){
|
||||
professions.forEach(function(p){
|
||||
const option = document.createElement("option");
|
||||
option.value = String(p.PRF_Incr);
|
||||
option.textContent = p.PRF_Libelle;
|
||||
// Insertion avant l'option "+ Nouvelle profession…", toujours en dernier.
|
||||
selectProfession.insertBefore(option, selectProfession.lastElementChild);
|
||||
});
|
||||
if(idExistant){
|
||||
selectProfession.value = String(idExistant);
|
||||
selectProfession.classList.add("interprete");
|
||||
} else if(libelleNouveau){
|
||||
selectProfession.value = "__nouvelle__";
|
||||
selectProfession.classList.add("interprete");
|
||||
blocNouvelleProfession.style.display = "block";
|
||||
champNouvelleProfession.value = libelleNouveau;
|
||||
}
|
||||
}
|
||||
|
||||
selectProfession.addEventListener("change", function(){
|
||||
blocNouvelleProfession.style.display = (selectProfession.value === "__nouvelle__") ? "block" : "none";
|
||||
});
|
||||
|
||||
boutonInterpreter.addEventListener("click", function(){
|
||||
if(!texteComplet) return;
|
||||
boutonInterpreter.disabled = true;
|
||||
boutonInterpreter.textContent = "Interprétation en cours…";
|
||||
messageCreation.textContent = "";
|
||||
messageCreation.className = "";
|
||||
|
||||
fetch("contact_ia_interprete.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({texte: texteComplet})
|
||||
}).then(function(reponse){
|
||||
return reponse.json().then(function(corps){
|
||||
if(!reponse.ok) throw new Error(corps.error || ("HTTP " + reponse.status));
|
||||
return corps;
|
||||
});
|
||||
}).then(function(corps){
|
||||
derniereExtraction = corps.extraction;
|
||||
viderFormulaire();
|
||||
remplirChampsTexte(corps.extraction.champs);
|
||||
remplirStatuts(corps.statuts, corps.extraction.champs.CNT_Statut);
|
||||
remplirProfessions(
|
||||
corps.professions,
|
||||
corps.extraction.profession_existante_id,
|
||||
corps.extraction.profession_nouvelle_libelle
|
||||
);
|
||||
zoneResultat.classList.add("visible");
|
||||
zoneResultat.scrollIntoView({behavior:"smooth", block:"start"});
|
||||
}).catch(function(erreur){
|
||||
afficherErreur("Échec de l'interprétation : " + erreur.message);
|
||||
}).finally(function(){
|
||||
boutonInterpreter.disabled = (texteComplet === "");
|
||||
boutonInterpreter.textContent = "Interpréter →";
|
||||
});
|
||||
});
|
||||
|
||||
// -------- Valider (4 septembre 2026) --------
|
||||
// Correction explicite de Jean-Marc : cet outil ne doit JAMAIS créer de
|
||||
// contact réel (ni de profession réelle au passage) — contrairement à ce
|
||||
// qu'une première version faisait en appelant contacts.php directement.
|
||||
// L'objectif de cette étape est seulement d'accumuler, pour chaque essai,
|
||||
// trois choses côte à côte (texte brut / ce que le LLM a compris / ce que
|
||||
// Jean-Marc valide après relecture-correction) afin d'en tirer des
|
||||
// conclusions sur la qualité de l'interprétation — et, plus tard, de
|
||||
// servir de matière à la création d'un post-it par assistant pour chaque
|
||||
// instruction vocale (vision "bureau à l'ancienne", pas encore câblée :
|
||||
// ASP_ToDoList ne sait pas encore attribuer une tâche à un assistant, voir
|
||||
// le "reste à faire" du journal de développement). Rien n'est donc écrit
|
||||
// dans ASP_Contacts ni ASP_Professions depuis cette page — uniquement dans
|
||||
// la nouvelle table ASP_PropositionsContact, via contact_ia_valide.php.
|
||||
function collecterChamps(){
|
||||
const champs = {};
|
||||
document.querySelectorAll("[data-champ]").forEach(function(el){
|
||||
champs[el.dataset.champ] = el.value.trim();
|
||||
});
|
||||
return champs;
|
||||
}
|
||||
|
||||
function collecterProfessionValidee(){
|
||||
if(selectProfession.value === "__nouvelle__"){
|
||||
return {profession_existante_id: null, profession_nouvelle_libelle: champNouvelleProfession.value.trim() || null};
|
||||
}
|
||||
if(selectProfession.value){
|
||||
return {profession_existante_id: parseInt(selectProfession.value, 10), profession_nouvelle_libelle: null};
|
||||
}
|
||||
return {profession_existante_id: null, profession_nouvelle_libelle: null};
|
||||
}
|
||||
|
||||
boutonCreer.addEventListener("click", function(){
|
||||
const champs = collecterChamps();
|
||||
if(!champs.CNT_Nom){
|
||||
messageCreation.className = "ko";
|
||||
messageCreation.textContent = "Le nom est obligatoire.";
|
||||
return;
|
||||
}
|
||||
boutonCreer.disabled = true;
|
||||
boutonCreer.textContent = "Enregistrement en cours…";
|
||||
messageCreation.textContent = "";
|
||||
messageCreation.className = "";
|
||||
|
||||
const valide = Object.assign({champs: champs}, collecterProfessionValidee());
|
||||
|
||||
fetch("contact_ia_valide.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
texteBrut: texteComplet,
|
||||
extraction: derniereExtraction,
|
||||
valide: valide
|
||||
})
|
||||
}).then(function(reponse){
|
||||
return reponse.json().then(function(corps){
|
||||
if(!reponse.ok) throw new Error(corps.error || ("HTTP " + reponse.status));
|
||||
return corps;
|
||||
});
|
||||
}).then(function(){
|
||||
messageCreation.className = "ok";
|
||||
messageCreation.textContent = "Essai enregistré (aucun contact réel créé) — merci !";
|
||||
}).catch(function(erreur){
|
||||
messageCreation.className = "ko";
|
||||
messageCreation.textContent = "Échec de l'enregistrement : " + erreur.message;
|
||||
}).finally(function(){
|
||||
boutonCreer.disabled = false;
|
||||
boutonCreer.textContent = "Valider";
|
||||
});
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user