mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 16:07:10 +00:00
V1.006 Suite de l'intégration de la voix
This commit is contained in:
@@ -60,13 +60,60 @@
|
||||
* 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).
|
||||
* tel quel dans la table ASP_ConsignesVoix (via lilou_notes.php ; le
|
||||
* fichier texte notes_lilou/dictees.txt de la V1.005 est abandonné depuis
|
||||
* le 2 septembre 2026, voir journal section 27) 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.
|
||||
*
|
||||
* Correctif du 2 septembre 2026 (premier essai réel en table SQL) : quand
|
||||
* l'ouverture ou la clôture/annulation est dite dans la même phrase que du
|
||||
* contenu, sans pause ("Lilou, ajoute une tâche, [contenu]" ou "[contenu],
|
||||
* c'est tout Lilou, [contenu]" tout d'une traite), le contenu qui suit la
|
||||
* phrase de contrôle repérée était perdu silencieusement — seul le contenu
|
||||
* qui la précédait (cas déjà traité pour la clôture depuis le 1er
|
||||
* septembre) était conservé. Corrigé pour capturer aussi ce qui suit,
|
||||
* côté ouverture ET clôture/annulation, symétriquement. Micro coupé
|
||||
* manuellement pendant une notation en cours (clôture orale oubliée ou non
|
||||
* reconnue) : traité désormais comme une clôture normale plutôt que comme
|
||||
* un abandon silencieux du contenu déjà dicté.
|
||||
*
|
||||
* Multi-agents et coupure pour inactivité (2 septembre 2026, suite du
|
||||
* premier essai réel en table SQL) : le mot déclencheur unique "Lilou" est
|
||||
* remplacé par une liste d'agents nommés (ASP_Agents : Lilou, Viktor,
|
||||
* Matilda, Léon, Karl, Nikita), chargée depuis agents.php au démarrage de
|
||||
* ce script — voir agentsDisponibles/chargerAgents plus bas. Le nom
|
||||
* reconnu en tête de la phrase d'ouverture détermine l'agent adressé pour
|
||||
* toute la notation qui suit (agentCourant), et part avec chaque ligne
|
||||
* enregistrée dans CVX_AgentTraitant (voir agent_notes.php, ex-lilou_notes.
|
||||
* php, renommé le même jour puisqu'il ne sert plus un seul agent).
|
||||
*
|
||||
* Variantes de prononciation (2 septembre 2026, généralisé après 3 cas
|
||||
* réels indépendants le même jour — Lilou/Milou/Loulou, Matilda/Mathilda,
|
||||
* Karl/Carl) : plutôt que du code en dur par agent, chaque ligne de
|
||||
* ASP_Agents porte une colonne AGN_Variantes (graphies alternatives
|
||||
* séparées par une virgule) intégrée au motif de reconnaissance — voir
|
||||
* construireAgent. Jean-Marc peut donc ajuster une graphie lui-même en
|
||||
* base, sans repasser par du code. Seule exception qui reste codée en dur :
|
||||
* l'homophone "les loups" pour la clôture de Lilou ("c'est tout Lilou" →
|
||||
* "c'est tous les loups"), qui n'est pas une variante du prénom seul mais
|
||||
* de la phrase de clôture entière. Règle à respecter en choisissant une
|
||||
* variante : uniquement des noms propres ou mots rares en tête de phrase,
|
||||
* jamais un mot français courant ("car", "calme"...) qui déclencherait
|
||||
* l'agent sur n'importe quelle phrase du quotidien commençant par ce mot —
|
||||
* refusé explicitement pour Karl ce jour-là malgré des essais réels ratés
|
||||
* avec "car" et "calme".
|
||||
*
|
||||
* Coupure pour inactivité (même jour) : par ailleurs,
|
||||
* suite à un incident réel où le micro est resté ouvert pendant un appel
|
||||
* téléphonique confidentiel (rien d'enregistré, sinon quelques traces de
|
||||
* debug déjà retirées depuis — voir plus bas), une coupure automatique du
|
||||
* micro après quelques minutes d'inactivité (DUREE_INACTIVITE_MS) a été
|
||||
* ajoutée, sur le même principe qu'une fermeture manuelle.
|
||||
*
|
||||
* É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
|
||||
@@ -123,13 +170,57 @@
|
||||
// le redémarrage automatique sur l'événement "end" qui suit.
|
||||
let erreurBloquante = false;
|
||||
|
||||
// Coupure automatique après inactivité (2 septembre 2026, demande de
|
||||
// Jean-Marc suite à l'incident du micro resté ouvert pendant un appel
|
||||
// téléphonique confidentiel — voir journal section 27) : si aucune
|
||||
// interaction réelle avec un agent (nom reconnu en tête de phrase, ou
|
||||
// ligne captée en mode notation) n'a eu lieu depuis DUREE_INACTIVITE_MS,
|
||||
// le micro se referme tout seul, comme un clic manuel de fermeture — pour
|
||||
// limiter la fenêtre pendant laquelle un micro oublié ouvert continue
|
||||
// d'envoyer l'audio ambiant au service de reconnaissance. Le bruit de
|
||||
// fond ignoré en silence (aucun agent reconnu) ne repousse volontairement
|
||||
// PAS ce délai : c'est précisément le cas qu'on veut détecter (une
|
||||
// conversation qui ne s'adresse à aucun agent). Durée choisie arbitraire
|
||||
// pour ce premier essai, à ajuster avec Jean-Marc selon l'usage réel.
|
||||
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, rien à faire
|
||||
// Même traitement que la coupure manuelle (voir le clic sur le bouton
|
||||
// plus bas) : une notation en cours n'est pas perdue silencieusement,
|
||||
// elle est clôturée avec un marqueur explicite avant l'arrêt.
|
||||
if(modeNotation){
|
||||
ajouterAuTampon("(coupure automatique pour inactivité)", "cloture");
|
||||
terminerNotation();
|
||||
}
|
||||
ecouteVoulue = false;
|
||||
notifier("Micro coupé automatiquement après " + Math.round(DUREE_INACTIVITE_MS / 60000) + " minutes d'inactivité.");
|
||||
reconnaissance.stop();
|
||||
}
|
||||
|
||||
function passerEnEcoute(){
|
||||
bouton.classList.add("ecoute");
|
||||
bouton.title = "Le micro est ouvert";
|
||||
reinitialiserMinuteurInactivite();
|
||||
}
|
||||
|
||||
function revenirAuRepos(){
|
||||
ecouteVoulue = false;
|
||||
arreterMinuteurInactivite();
|
||||
bouton.classList.remove("ecoute");
|
||||
bouton.title = "Le micro est inactif";
|
||||
}
|
||||
@@ -140,6 +231,18 @@
|
||||
// via l'événement "end" qui va suivre, sans redémarrage puisque
|
||||
// ecouteVoulue passe à faux avant l'appel à stop().
|
||||
ecouteVoulue = false;
|
||||
// Coupure manuelle du micro alors qu'une notation était en cours
|
||||
// (clôture orale oubliée, ou "c'est tout Lilou" non reconnu) : on ne
|
||||
// perd pas le contenu déjà dicté — même traitement qu'une clôture
|
||||
// normale, plutôt qu'un abandon silencieux (retour de Jean-Marc, 2
|
||||
// septembre 2026 : du contenu dicté ne s'est pas retrouvé enregistré).
|
||||
// Marqueur de clôture explicite ajouté (type "cloture") pour que le
|
||||
// lot envoyé garde la même forme qu'une clôture orale, même sans
|
||||
// phrase de clôture réellement entendue.
|
||||
if(modeNotation){
|
||||
ajouterAuTampon("(coupure manuelle du micro)", "cloture");
|
||||
terminerNotation();
|
||||
}
|
||||
reconnaissance.stop();
|
||||
return;
|
||||
}
|
||||
@@ -164,6 +267,7 @@
|
||||
// -------- 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 panneauTitre = null;
|
||||
let listeNotation = null;
|
||||
|
||||
function construirePanneauNotation(){
|
||||
@@ -185,13 +289,20 @@
|
||||
panneauNotation = document.createElement("div");
|
||||
panneauNotation.id = "panneau-notation-lilou";
|
||||
panneauNotation.innerHTML =
|
||||
'<div class="titre">Lilou vous écoute…</div><ul class="lignes"></ul>';
|
||||
'<div class="titre"></div><ul class="lignes"></ul>';
|
||||
document.body.appendChild(panneauNotation);
|
||||
panneauTitre = panneauNotation.querySelector(".titre");
|
||||
listeNotation = panneauNotation.querySelector(".lignes");
|
||||
}
|
||||
|
||||
function ouvrirPanneauNotation(){
|
||||
// nomAgent : titre du panneau adapté à l'agent adressé (2 septembre
|
||||
// 2026, plusieurs agents désormais possibles — voir ASP_Agents). Repli
|
||||
// générique "L'agent" si absent (ne devrait pas arriver, demarrerNotation
|
||||
// fournit toujours un agent détecté) — plus de nom d'agent précis figé en
|
||||
// dur ici depuis le retrait de Lilou de ASP_Agents le 2 septembre 2026.
|
||||
function ouvrirPanneauNotation(nomAgent){
|
||||
construirePanneauNotation();
|
||||
panneauTitre.textContent = (nomAgent || "L'agent") + " vous écoute…";
|
||||
listeNotation.innerHTML = "";
|
||||
panneauNotation.classList.add("visible");
|
||||
}
|
||||
@@ -223,16 +334,14 @@
|
||||
let reveilEnAttente = false;
|
||||
let expirationReveil = 0;
|
||||
const DUREE_REVEIL_MS = 6000;
|
||||
// agentReveil : agent dont le nom seul vient d'être entendu, mémorisé
|
||||
// pour attribuer correctement le résultat suivant à l'intérieur de la
|
||||
// fenêtre ci-dessus (2 septembre 2026 — généralisation multi-agents de ce
|
||||
// mécanisme, qui ne connaissait que Lilou jusqu'ici).
|
||||
let agentReveil = null;
|
||||
|
||||
// Le moteur de reconnaissance confond parfois "Lilou" avec "Milou" (même
|
||||
// sonorité, et "Milou" est un mot bien plus courant en français...) —
|
||||
// tous les motifs qui doivent repérer "Lilou" ci-dessous acceptent aussi
|
||||
// "Milou", pour ne pas bloquer tout le circuit sur un simple aléa de
|
||||
// transcription (retour de Jean-Marc, 2 septembre 2026 : "il a cru que je
|
||||
// disais c'est tout Milou").
|
||||
const MOT_LILOU = "(?:li|mi)lou";
|
||||
|
||||
// Phrases d'ouverture : testées sur le texte après retrait de "Lilou".
|
||||
// Phrases d'ouverture : testées sur le texte après retrait du nom de
|
||||
// l'agent (indépendantes de l'agent adressé).
|
||||
const PHRASES_OUVERTURE = [/tâche/i, /truc à faire/i, /note.*todolist/i];
|
||||
// Phrases de clôture / annulation : recherchées n'importe où dans la
|
||||
// phrase entendue, pas seulement en tête (ajusté le 1er septembre 2026
|
||||
@@ -241,20 +350,109 @@
|
||||
// naturel à l'oral — "date de réalisation le 12/09 c'est tout Lilou" doit
|
||||
// fonctionner dit d'une traite). Ce qui précède la phrase repérée dans la
|
||||
// même phrase entendue est conservé comme dernière ligne de contenu avant
|
||||
// de clore — voir traiterLigneNotation.
|
||||
const PHRASES_CLOTURE = [new RegExp("c'?est tout " + MOT_LILOU, "i")];
|
||||
const PHRASES_ANNULATION = [new RegExp(MOT_LILOU + "\\s+annule", "i")];
|
||||
// de clore — voir traiterLigneNotation. Construites par agent ci-dessous
|
||||
// (2 septembre 2026, ASP_Agents) plutôt qu'une seule fois pour "Lilou".
|
||||
|
||||
// -------- Agents (ASP_Agents) — assistants nommés auxquels une consigne
|
||||
// -------- vocale peut être adressée (2 septembre 2026, demande de
|
||||
// -------- Jean-Marc, voir journal section 27 et réflexions stratégiques).
|
||||
// -------- Liste récupérée depuis le serveur au chargement plutôt que
|
||||
// -------- codée en dur, pour qu'ajouter/retirer un agent dans ASP_Agents
|
||||
// -------- n'exige pas de modifier ce fichier.
|
||||
let agentsDisponibles = [];
|
||||
let agentsPrets = false;
|
||||
|
||||
// Échappe un nom d'agent pour un usage sûr en expression régulière (au
|
||||
// cas où un nom contiendrait un jour un caractère spécial).
|
||||
function echapperRegex(texte){
|
||||
return texte.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
// Construit les motifs de reconnaissance pour un agent donné, à partir de
|
||||
// son nom et de ses variantes connues (colonne AGN_Variantes — voir
|
||||
// commentaire d'en-tête, "Variantes de prononciation"). variantesBrutes
|
||||
// est la chaîne brute reçue de agents.php ("Milou,Loulou"), vide ou
|
||||
// absente pour un agent sans variante connue pour l'instant.
|
||||
function construireAgent(nom, variantesBrutes){
|
||||
const variantes = (variantesBrutes || "")
|
||||
.split(",")
|
||||
.map(function(v){ return v.trim(); })
|
||||
.filter(function(v){ return v !== ""; });
|
||||
const formes = [nom].concat(variantes).map(echapperRegex);
|
||||
const motNom = formes.length > 1 ? "(?:" + formes.join("|") + ")" : formes[0];
|
||||
|
||||
// Seul cas qui reste codé en dur (voir commentaire d'en-tête) :
|
||||
// l'homophone "les loups" pour la clôture/annulation de Lilou, qui
|
||||
// porte sur la phrase entière et pas sur le seul prénom.
|
||||
let motifsClotureSupp = [];
|
||||
let motifsAnnulationSupp = [];
|
||||
if(/^lilou$/i.test(nom)){
|
||||
motifsClotureSupp = [/c'?est tou[st] les loups/i];
|
||||
motifsAnnulationSupp = [/les loups\s+annule/i];
|
||||
}
|
||||
|
||||
return {
|
||||
nom: nom,
|
||||
motifOuverture: new RegExp("^" + motNom + "\\s*[,:]?\\s*", "i"),
|
||||
motifsCloture: [new RegExp("c'?est tout " + motNom, "i")].concat(motifsClotureSupp),
|
||||
motifsAnnulation: [new RegExp(motNom + "\\s+annule", "i")].concat(motifsAnnulationSupp)
|
||||
};
|
||||
}
|
||||
|
||||
// Repère, en tête du texte donné, le nom d'un agent connu — renvoie
|
||||
// l'agent et le texte restant (commande) une fois le nom retiré, ou null
|
||||
// si aucun agent connu n'est reconnu en tête de phrase.
|
||||
function trouverAgentDeclencheur(texte){
|
||||
for(let i = 0; i < agentsDisponibles.length; i++){
|
||||
const agent = agentsDisponibles[i];
|
||||
if(agent.motifOuverture.test(texte)){
|
||||
return {agent: agent, commande: texte.replace(agent.motifOuverture, "").trim()};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function chargerAgents(){
|
||||
fetch("agents.php")
|
||||
.then(function(reponse){
|
||||
if(!reponse.ok) throw new Error("HTTP " + reponse.status);
|
||||
return reponse.json();
|
||||
})
|
||||
.then(function(donnees){
|
||||
const liste = (donnees && donnees.agents) || [];
|
||||
if(liste.length === 0) throw new Error("liste vide");
|
||||
agentsDisponibles = liste.map(function(a){ return construireAgent(a.AGN_Nom, a.AGN_Variantes); });
|
||||
agentsPrets = true;
|
||||
})
|
||||
.catch(function(){
|
||||
// Plus de repli sur un nom d'agent figé en dur ici (2 septembre
|
||||
// 2026, décision de Jean-Marc après le retrait de Lilou de
|
||||
// ASP_Agents — un agent codé en dur ici referait exactement le
|
||||
// problème qu'on vient de corriger côté agent_notes.php). Si
|
||||
// agents.php est injoignable, la liste reste vide : aucune phrase
|
||||
// ne déclenche de notation tant que la page n'est pas rechargée —
|
||||
// même comportement que du bruit de fond ignoré, plutôt que de
|
||||
// prétendre qu'un agent précis est disponible alors que la vraie
|
||||
// liste n'a pas pu être chargée. Seule différence avec le bruit de
|
||||
// fond ordinaire : un avertissement explicite, pour que ça ne
|
||||
// passe pas inaperçu.
|
||||
agentsDisponibles = [];
|
||||
agentsPrets = true;
|
||||
notifier("Liste des agents indisponible — aucun agent ne sera reconnu tant que la page n'est pas rechargée.");
|
||||
});
|
||||
}
|
||||
chargerAgents();
|
||||
|
||||
// Marqueur oral de fin de ligne ("..., ensuite") retiré du texte conservé.
|
||||
const MARQUEUR_FIN_LIGNE = /\s*,?\s*ensuite\s*[.,!]?\s*$/i;
|
||||
|
||||
// Repère la première occurrence (n'importe où dans le texte) d'une phrase
|
||||
// de la liste donnée, et renvoie sa position — ou null si aucune ne
|
||||
// correspond.
|
||||
// de la liste donnée, et renvoie sa position et sa longueur — ou null si
|
||||
// aucune ne correspond.
|
||||
function trouverPhraseDeControle(texte, motifs){
|
||||
for(let i = 0; i < motifs.length; i++){
|
||||
const correspondance = texte.match(motifs[i]);
|
||||
if(correspondance) return {index: correspondance.index};
|
||||
if(correspondance) return {index: correspondance.index, longueur: correspondance[0].length};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -267,24 +465,66 @@
|
||||
return texte.replace(MARQUEUR_FIN_LIGNE, "").replace(/[\s,;:]+$/, "").trim();
|
||||
}
|
||||
|
||||
// Même nettoyage, côté début de ligne cette fois : retire la ponctuation
|
||||
// de liaison isolée qui suit souvent une phrase de contrôle repérée en
|
||||
// milieu de phrase ("... tâche : penser à ..." → ": " à retirer avant de
|
||||
// garder "penser à ...").
|
||||
function nettoyerDebutLigne(texte){
|
||||
return texte.replace(/^[\s:,;-]+/, "").trim();
|
||||
}
|
||||
|
||||
// Empile une ligne dans le tampon de notation avec son type ("ouverture",
|
||||
// "contenu", "cloture" ou "annulation" — demande de Jean-Marc, 2
|
||||
// septembre 2026, pour qu'un futur robot sache à quel assistant/action
|
||||
// router le lot sans avoir à ré-analyser le texte). Seul le contenu
|
||||
// s'affiche dans le panneau visuel — les marqueurs d'ouverture/clôture
|
||||
// sont enregistrés mais ne servent à rien à afficher à l'utilisateur.
|
||||
function ajouterAuTampon(texte, type){
|
||||
if(!texte) return;
|
||||
bufferNotation.push({texte: texte, type: type});
|
||||
if(type === "contenu"){
|
||||
afficherLigneDansPanneau(texte);
|
||||
}
|
||||
}
|
||||
|
||||
function envoyerNotation(lignes){
|
||||
fetch("lilou_notes.php", {
|
||||
// Renommé le 2 septembre 2026 (ex-lilou_notes.php) — plus de raison de
|
||||
// porter le nom d'un seul agent, voir agent_notes.php. "agent" : nom
|
||||
// de l'assistant adressé pour tout ce lot (agentCourant, capturé à
|
||||
// l'ouverture), copié dans CVX_AgentTraitant pour chaque ligne. Chaîne
|
||||
// vide si agentCourant est absent (ne devrait pas arriver) plutôt qu'un
|
||||
// nom d'agent précis figé en dur ici : agent_notes.php sait désormais
|
||||
// résoudre lui-même l'agent par défaut depuis ASP_Agents.AGN_Defaut
|
||||
// (voir ce fichier, 2 septembre 2026, suite du retrait de Lilou).
|
||||
fetch("agent_notes.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({lignes: lignes})
|
||||
body: JSON.stringify({agent: agentCourant ? agentCourant.nom : "", lignes: lignes})
|
||||
}).then(function(reponse){
|
||||
if(!reponse.ok) throw new Error("HTTP " + reponse.status);
|
||||
notifier("Note enregistrée.");
|
||||
// Message rendu explicite à la demande de Jean-Marc (2 septembre
|
||||
// 2026), pour ne laisser aucun doute sur le fait que l'enregistrement
|
||||
// a bien eu lieu, et où : la table ASP_ConsignesVoix, qualifiée de
|
||||
// "provisoire" (nom générique, avant les règles d'interprétation de
|
||||
// l'étape 2).
|
||||
notifier("Consigne enregistrée dans la table provisoire");
|
||||
}).catch(function(){
|
||||
notifier("Erreur : la note n'a pas pu être enregistrée.");
|
||||
});
|
||||
}
|
||||
|
||||
function demarrerNotation(){
|
||||
// agentCourant : agent adressé pour la notation en cours (objet construit
|
||||
// par construireAgent) — capturé à l'ouverture, sert aussi bien pour la
|
||||
// reconnaissance de la clôture/annulation propre à cet agent que pour
|
||||
// l'envoi final (2 septembre 2026, ASP_Agents).
|
||||
let agentCourant = null;
|
||||
|
||||
function demarrerNotation(agent){
|
||||
modeNotation = true;
|
||||
bufferNotation = [];
|
||||
ouvrirPanneauNotation();
|
||||
notifier('Lilou note votre demande — dites "c\'est tout Lilou" pour terminer, ou "Lilou annule" pour annuler.');
|
||||
agentCourant = agent;
|
||||
ouvrirPanneauNotation(agent.nom);
|
||||
notifier(agent.nom + ' note votre demande — dites "c\'est tout ' + agent.nom + '" pour terminer, ou "' + agent.nom + ' annule" pour annuler.');
|
||||
}
|
||||
|
||||
function terminerNotation(){
|
||||
@@ -297,29 +537,44 @@
|
||||
}
|
||||
|
||||
function traiterLigneNotation(texte){
|
||||
const cloture = trouverPhraseDeControle(texte, PHRASES_CLOTURE);
|
||||
const annulation = cloture ? null : trouverPhraseDeControle(texte, PHRASES_ANNULATION);
|
||||
// Clôture/annulation propres à l'agent en cours (2 septembre 2026) —
|
||||
// "c'est tout Viktor" ne referme pas une notation ouverte avec Lilou,
|
||||
// et réciproquement.
|
||||
const motifsCloture = agentCourant ? agentCourant.motifsCloture : [];
|
||||
const motifsAnnulation = agentCourant ? agentCourant.motifsAnnulation : [];
|
||||
const cloture = trouverPhraseDeControle(texte, motifsCloture);
|
||||
const annulation = cloture ? null : trouverPhraseDeControle(texte, motifsAnnulation);
|
||||
const repere = cloture || annulation;
|
||||
if(repere){
|
||||
// Même comportement dans les deux cas : on enregistre ce qui a été
|
||||
// dit, sans traitement différencié (demande explicite de Jean-Marc).
|
||||
// Tout ce qui précède la phrase de clôture/annulation dans cette même
|
||||
// phrase entendue est la dernière ligne de contenu ("date de
|
||||
// réalisation le 12/09 c'est tout Lilou" dit d'une traite) : on la
|
||||
// capture avant de clore.
|
||||
const avant = nettoyerLigne(texte.slice(0, repere.index));
|
||||
if(avant){
|
||||
bufferNotation.push(avant);
|
||||
afficherLigneDansPanneau(avant);
|
||||
}
|
||||
ajouterAuTampon(avant, "contenu");
|
||||
|
||||
// La phrase de contrôle elle-même est maintenant enregistrée aussi
|
||||
// (demande de Jean-Marc, 2 septembre 2026), avec son type — cloture
|
||||
// ou annulation distinguées cette fois (alors que le traitement du
|
||||
// contenu, lui, reste identique dans les deux cas, comme décidé le
|
||||
// 1er septembre) : un futur robot doit savoir si le lot est à
|
||||
// exécuter ou à écarter sans avoir à ré-interpréter le texte brut.
|
||||
const phraseControle = texte.slice(repere.index, repere.index + repere.longueur).trim();
|
||||
ajouterAuTampon(phraseControle, cloture ? "cloture" : "annulation");
|
||||
|
||||
// Symétriquement, ce qui suit la phrase de clôture/annulation dans la
|
||||
// même phrase entendue ("c'est tout Lilou, et puis aussi ...") était
|
||||
// jusqu'ici perdu silencieusement. Philosophie du projet : on
|
||||
// n'interprète rien, on note tout ce qui a été dit (retour de
|
||||
// Jean-Marc, 2 septembre 2026 — voir aussi le symétrique côté
|
||||
// ouverture ci-dessous).
|
||||
const apres = nettoyerLigne(nettoyerDebutLigne(texte.slice(repere.index + repere.longueur)));
|
||||
ajouterAuTampon(apres, "contenu");
|
||||
|
||||
terminerNotation();
|
||||
return;
|
||||
}
|
||||
const ligne = nettoyerLigne(texte);
|
||||
if(ligne){
|
||||
bufferNotation.push(ligne);
|
||||
afficherLigneDansPanneau(ligne);
|
||||
}
|
||||
ajouterAuTampon(nettoyerLigne(texte), "contenu");
|
||||
}
|
||||
|
||||
reconnaissance.addEventListener("start", passerEnEcoute);
|
||||
@@ -340,59 +595,95 @@
|
||||
revenirAuRepos();
|
||||
});
|
||||
|
||||
// Mot déclencheur en tête de phrase, avec ou sans virgule/deux-points
|
||||
// juste après ("Lilou, ajoute une tâche" ou "Lilou ajoute une tâche") —
|
||||
// "Milou" toléré au même titre que les autres motifs, voir MOT_LILOU.
|
||||
const motifDeclencheur = new RegExp("^" + MOT_LILOU + "\\s*[,:]?\\s*", "i");
|
||||
// Détection du mot déclencheur (2 septembre 2026, généralisation
|
||||
// multi-agents) : plus un seul motifDeclencheur codé en dur pour Lilou,
|
||||
// mais trouverAgentDeclencheur(texte) qui teste chaque agent connu
|
||||
// (agentsDisponibles, chargé depuis ASP_Agents — voir plus haut) et
|
||||
// renvoie celui reconnu en tête de phrase, le cas échéant.
|
||||
|
||||
reconnaissance.addEventListener("result", function(evenement){
|
||||
const dernier = evenement.results[evenement.results.length - 1];
|
||||
const texte = dernier[0].transcript.trim();
|
||||
|
||||
if(modeNotation){
|
||||
// En notation, plus besoin de "Lilou" en tête sur chaque ligne — on
|
||||
// ne teste que la clôture/annulation, tout le reste est du contenu.
|
||||
// En notation, plus besoin du nom de l'agent en tête sur chaque
|
||||
// ligne — on ne teste que la clôture/annulation propre à
|
||||
// agentCourant, tout le reste est du contenu. Une ligne captée ici
|
||||
// est une vraie interaction : le délai d'inactivité est repoussé.
|
||||
traiterLigneNotation(texte);
|
||||
reinitialiserMinuteurInactivite();
|
||||
return;
|
||||
}
|
||||
|
||||
let agent;
|
||||
let commande;
|
||||
if(motifDeclencheur.test(texte)){
|
||||
commande = texte.replace(motifDeclencheur, "").trim();
|
||||
} else if(reveilEnAttente && Date.now() <= expirationReveil){
|
||||
// "Lilou" venait d'être entendu seul : on accepte ce résultat-ci
|
||||
// comme s'il avait été précédé du mot déclencheur (voir commentaire
|
||||
// sur reveilEnAttente plus haut).
|
||||
const declenchement = trouverAgentDeclencheur(texte);
|
||||
if(declenchement){
|
||||
agent = declenchement.agent;
|
||||
commande = declenchement.commande;
|
||||
} else if(reveilEnAttente && agentReveil && Date.now() <= expirationReveil){
|
||||
// Un nom d'agent venait d'être entendu seul : on accepte ce
|
||||
// résultat-ci comme s'il avait été précédé du nom, pour le même
|
||||
// agent (voir commentaire sur reveilEnAttente/agentReveil plus haut).
|
||||
agent = agentReveil;
|
||||
commande = texte;
|
||||
} else {
|
||||
// Pas adressé à Lilou (conversation de fond, etc.) : ignoré en
|
||||
// silence, sans notification — c'est tout l'intérêt du mot
|
||||
// déclencheur en écoute continue. Laissé en console pour le debug.
|
||||
console.log('[micro.js] ignoré (pas de "Lilou" en tête) :', texte);
|
||||
// Pas adressé à un agent connu (conversation de fond, etc.) : ignoré
|
||||
// en silence, sans notification, et SANS repousser le délai
|
||||
// d'inactivité — c'est justement ce que ce délai doit détecter (voir
|
||||
// couperPourInactivite plus haut). Laissé en console pour le debug.
|
||||
console.log('[micro.js] ignoré (aucun agent reconnu en tête) :', texte);
|
||||
return;
|
||||
}
|
||||
reveilEnAttente = false;
|
||||
agentReveil = null;
|
||||
reinitialiserMinuteurInactivite();
|
||||
|
||||
if(!commande){
|
||||
// "Lilou" entendu seul, rien d'exploitable derrière dans ce même
|
||||
// Nom d'agent entendu seul, rien d'exploitable derrière dans ce même
|
||||
// résultat — probablement coupé par le moteur juste après la
|
||||
// virgule. On laisse une petite fenêtre pour que la suite, dite
|
||||
// juste après, soit acceptée même sans "Lilou" en tête.
|
||||
// juste après, soit acceptée même sans le nom en tête, et on
|
||||
// mémorise l'agent concerné (agentReveil) pour l'attribuer
|
||||
// correctement.
|
||||
reveilEnAttente = true;
|
||||
agentReveil = agent;
|
||||
expirationReveil = Date.now() + DUREE_REVEIL_MS;
|
||||
notifier("Lilou vous écoute.");
|
||||
notifier(agent.nom + " vous écoute.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(PHRASES_OUVERTURE.some(function(m){ return m.test(commande); })){
|
||||
demarrerNotation();
|
||||
const repereOuverture = trouverPhraseDeControle(commande, PHRASES_OUVERTURE);
|
||||
if(repereOuverture){
|
||||
demarrerNotation(agent);
|
||||
// La phrase d'ouverture elle-même est enregistrée (type "ouverture",
|
||||
// demande de Jean-Marc le 2 septembre 2026) — texte complet jusqu'à
|
||||
// la fin de la phrase repérée ("ajoute une tâche", "un truc à
|
||||
// faire"...), pour qu'un futur robot puisse s'en servir pour choisir
|
||||
// le bon assistant, sans que Claude/l'agent n'ait à l'interpréter ici.
|
||||
const phraseOuverture = commande.slice(0, repereOuverture.index + repereOuverture.longueur).trim();
|
||||
ajouterAuTampon(phraseOuverture, "ouverture");
|
||||
|
||||
// Contenu éventuellement dicté dans la même phrase que l'ouverture
|
||||
// ("Lilou, ajoute une tâche, penser à ..." dit d'une traite, sans
|
||||
// pause) : jusqu'ici perdu silencieusement — demarrerNotation() ne
|
||||
// fait que réinitialiser le tampon et ouvrir le panneau, rien
|
||||
// n'était capturé pour ce premier morceau. Corrigé le 2 septembre
|
||||
// 2026 (retour de Jean-Marc : "texte de départ... pas noté, qui
|
||||
// pourtant s'était affiché") en reprenant le même principe que la
|
||||
// clôture juste au-dessus : tout ce qui suit la phrase d'ouverture
|
||||
// repérée devient la première ligne de contenu.
|
||||
const apres = nettoyerLigne(nettoyerDebutLigne(
|
||||
commande.slice(repereOuverture.index + repereOuverture.longueur)
|
||||
));
|
||||
ajouterAuTampon(apres, "contenu");
|
||||
return;
|
||||
}
|
||||
|
||||
// Étape 1 (hors notation) : simple retour visuel du texte reconnu
|
||||
// après le mot déclencheur, pour valider le circuit — aucune autre
|
||||
// après le nom de l'agent, pour valider le circuit — aucune autre
|
||||
// commande interprétée à ce stade.
|
||||
notifier('Lilou a entendu : "' + commande + '"');
|
||||
notifier(agent.nom + ' a entendu : "' + commande + '"');
|
||||
});
|
||||
|
||||
reconnaissance.addEventListener("error", function(evenement){
|
||||
|
||||
Reference in New Issue
Block a user