synthèse vocale
This commit is contained in:
206
index.html
Normal file
206
index.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chatβeta*</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<link rel="icon" type="image/svg+xml" href="cat.svg">
|
||||
<link rel="apple-touch-icon" sizes="240x240" href="thumbnail.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="Chatβeta*" />
|
||||
<meta property="og:title" content="😸 Chatβeta*" />
|
||||
<meta property="og:type" content="game" />
|
||||
<meta property="og:locale" content="fr_FR" />
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
@media (min-height: 600px) {
|
||||
body > header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
background: linear-gradient(var(--pico-background-color) 60%, transparent);
|
||||
text-shadow: 0 0 10px var(--pico-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
body > header .container {
|
||||
max-width: 750px;
|
||||
}
|
||||
|
||||
body > main {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.reponse::before {
|
||||
content: "😸 ";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<h1>Chat<small><em>βeta</em><span style="color: #9B2318">*</span></small></h1>
|
||||
<button id="bouton_synthese_vocale" type="button" class="outline" style="display: none;">🕨</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="container overflow-auto" id="conversation">
|
||||
<p class="reponse">Posez-moi toutes vos questions !</p>
|
||||
</main>
|
||||
<footer class="container">
|
||||
<form id="formulaire" action="question.php" method="post" role="group">
|
||||
<textarea id="question" name="question" placeholder="Ma question" required></textarea>
|
||||
<button type="submit">Envoyer</button>
|
||||
</form>
|
||||
</footer>
|
||||
<dialog id="boite_synthese_vocale">
|
||||
<article>
|
||||
<header>
|
||||
<button id="bouton_fermer" aria-label="Close" rel="prev"></button>
|
||||
<p>
|
||||
<strong>🕨 Synthèse vocale</strong>
|
||||
</p>
|
||||
</header>
|
||||
<form>
|
||||
<fieldset>
|
||||
<label>Voix disponibles</label>
|
||||
<select id="select_voix">
|
||||
<option value="">Pas de synthèse vocale</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
</form>
|
||||
<footer>
|
||||
<button id="bouton_annuler" class="secondary">Annuler</button>
|
||||
<button id="bouton_ok">OK</button>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
<script>
|
||||
const formulaire = document.getElementById('formulaire');
|
||||
const bouton = document.querySelector('button[type="submit"]');
|
||||
const conversation = document.getElementById('conversation');
|
||||
const question = document.getElementById('question');
|
||||
const bouton_synthese_vocale = document.getElementById('bouton_synthese_vocale');
|
||||
const boite_synthese_vocale = document.getElementById('boite_synthese_vocale');
|
||||
const select_voix = document.getElementById('select_voix');
|
||||
const bouton_fermer = document.getElementById('bouton_fermer');
|
||||
const bouton_annuler = document.getElementById('bouton_annuler');
|
||||
const bouton_ok = document.getElementById('bouton_ok');
|
||||
const langue = "fr-FR";
|
||||
|
||||
let voix = null;
|
||||
if ('speechSynthesis' in window) {
|
||||
let liste_voix = speechSynthesis.getVoices().filter(voice => voice.lang === langue);
|
||||
liste_voix.forEach((v, i) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = i;
|
||||
option.innerText = v.name;
|
||||
select_voix.appendChild(option);
|
||||
if (v.voiceURI === window.localStorage.getItem('voix')) {
|
||||
select_voix.value = i;
|
||||
voix = v;
|
||||
option.selected = true;
|
||||
}
|
||||
})
|
||||
|
||||
bouton_synthese_vocale.style.display = 'block';
|
||||
bouton_synthese_vocale.addEventListener('click', () => {
|
||||
boite_synthese_vocale.showModal();
|
||||
});
|
||||
|
||||
bouton_fermer.addEventListener('click', () => {
|
||||
boite_synthese_vocale.close();
|
||||
});
|
||||
bouton_annuler.addEventListener('click', () => {
|
||||
boite_synthese_vocale.close();
|
||||
});
|
||||
bouton_ok.addEventListener('click', () => {
|
||||
boite_synthese_vocale.close();
|
||||
if (select_voix.value) {
|
||||
voix = liste_voix[select_voix.value];
|
||||
window.localStorage.setItem('voix', voix.voiceURI);
|
||||
} else {
|
||||
voix = null;
|
||||
window.localStorage.removeItem('voix');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
question.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
e.target.form.requestSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
question.addEventListener('focus', () => {
|
||||
question.scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
|
||||
formulaire.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (bouton.disabled == true) return;
|
||||
|
||||
bouton.disabled = true;
|
||||
bouton.setAttribute("aria-busy", true)
|
||||
const formulaireData = new FormData(formulaire);
|
||||
const citation = document.createElement('article');
|
||||
citation.innerText = formulaireData.get('question');
|
||||
conversation.appendChild(citation);
|
||||
formulaire.reset();
|
||||
|
||||
const paragraphe = document.createElement('p');
|
||||
paragraphe.setAttribute("aria-busy", "true");
|
||||
conversation.appendChild(paragraphe);
|
||||
conversation.scrollTop = conversation.scrollHeight;
|
||||
|
||||
const requete = await fetch(formulaire.action, {
|
||||
method: formulaire.method,
|
||||
body: formulaireData
|
||||
});
|
||||
paragraphe.setAttribute('aria-busy', 'false');
|
||||
const reponse = await requete.text();
|
||||
paragraphe.setAttribute('aria-busy', 'false');
|
||||
|
||||
if (voix) {
|
||||
const utterance = new SpeechSynthesisUtterance(reponse);
|
||||
utterance.lang = langue;
|
||||
utterance.voice = voix;
|
||||
utterance.rate = 1;
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
let t = 0;
|
||||
Array.from(reponse).forEach((lettre, i) => {
|
||||
setTimeout(() => {
|
||||
if (lettre == "\n") {
|
||||
paragraphe.innerHTML += "<br>";
|
||||
} else {
|
||||
paragraphe.innerHTML += lettre;
|
||||
}
|
||||
conversation.scrollTop = conversation.scrollHeight;
|
||||
}, t += 100 * Math.random());
|
||||
});
|
||||
setTimeout(() => {
|
||||
conversation.innerHTML += '<p class="reponse">Voulez-vous que je réponde à une autre question ?</p>';
|
||||
conversation.scrollTop = conversation.scrollHeight;
|
||||
bouton.disabled = false;
|
||||
bouton.setAttribute("aria-busy", false);
|
||||
question.focus()
|
||||
}, t);
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user