synthèse vocale

This commit is contained in:
2026-01-17 23:49:13 +01:00
parent a1606819a1
commit ee3c443464
5 changed files with 262 additions and 124 deletions

View File

@@ -15,7 +15,8 @@ C’est la relation à la parole et l’aliénation du sujet en tant que nous y av
Tout est très différent. Tout est très différent.
Les arbres longeant la caserne, seront éclairés, pour déboucher la perspective montante dans l'avenue Robert Schuman. Les arbres longeant la caserne, seront éclairés, pour déboucher la perspective montante dans l'avenue Robert Schuman.
La jeune femme, belle voix, beaux gestes, débute par J'ai un faible pour les forts (1932), un classique souvent traité en marche de cavalerie. La jeune femme, belle voix, beaux gestes, débute par J'ai un faible pour les forts (1932), un classique souvent traité en marche de cavalerie.
Le premier film de la série était tout de même regardable, mais on dirait que cela va en s'empirant d'un film à l'autre.Je ne veux plus rien savoir de cette série que je trouve de moins en moins drôle et de plus en plus prétentieuse. Le premier film de la série était tout de même regardable, mais on dirait que cela va en s'empirant d'un film à l'autre.
Je ne veux plus rien savoir de cette série que je trouve de moins en moins drôle et de plus en plus prétentieuse.
Le nouveau complexe de jeux rêvé par Loto-Québec comprendrait un hôtel de luxe de 350 chambres et le casino serait souterrain. Le nouveau complexe de jeux rêvé par Loto-Québec comprendrait un hôtel de luxe de 350 chambres et le casino serait souterrain.
Dwight Freeney et ses coéquipiers en défensive veulent prouver qu'ils peuvent stopper la course. Dwight Freeney et ses coéquipiers en défensive veulent prouver qu'ils peuvent stopper la course.
Avec l'arrivée de Paul Painlevé à la présidence du Conseil, le sous-secrétariat d'Etat passe sous la tutelle du ministère de la guerre, à la tête duquel se trouve Painlevé, et est rattaché à la section technique du génie. Avec l'arrivée de Paul Painlevé à la présidence du Conseil, le sous-secrétariat d'Etat passe sous la tutelle du ministère de la guerre, à la tête duquel se trouve Painlevé, et est rattaché à la section technique du génie.

206
index.html Normal file
View 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>

View File

@@ -1,88 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatEB</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">
<style>
main {
display: flex;
flex-direction: column;
height: 100vh;
}
#conversation {
flex-grow: 2;
overflow-y: auto;
}
</style>
</head>
<body>
<main class="container">
<h1>ChatEB</h1>
<div id="conversation">
<p>😸 Posez-moi toutes vos questions !</p>
</div>
<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>
</main>
<script>
const formulaire = document.getElementById('formulaire');
const bouton = document.querySelector('button[type="submit"]');
const conversation = document.getElementById('conversation');
const question = document.getElementById('question');
question.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey) {
e.preventDefault();
e.target.form.requestSubmit();
}
});
formulaire.addEventListener('submit', async (e) => {
e.preventDefault();
bouton.disabled = true;
bouton.setAttribute("aria-busy", true)
const formulaireData = new FormData(formulaire);
const citation = document.createElement('blockquote');
citation.innerText = formulaireData.get('question');
conversation.appendChild(citation);
formulaire.reset();
const paragraphe = document.createElement('p');
paragraphe.setAttribute('aria-busy', 'true');
conversation.appendChild(paragraphe);
const requete = await fetch(formulaire.action, {
method: formulaire.method,
body: formulaireData
});
paragraphe.setAttribute('aria-busy', 'false');
const reader = requete.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const { value, done } = await reader.read();
if (done) break;
const lettre = decoder.decode(value, { stream: true });
if (lettre == "\n") paragraphe.innerHTML += "<br>";
else paragraphe.innerHTML += lettre;
await new Promise(r => setTimeout(r, 0)); // micro pause pour rendre l'UI responsive
}
conversation.innerHTML += "<p>😸 Voulez-vous que je réponde à une autre question ?</p>";
conversation.scrollTop = conversation.scrollHeight;
bouton.disabled = false;
bouton.setAttribute("aria-busy", false);
});
</script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
from collections import defaultdict
from random import choice, randrange
suivants = defaultdict(list)
with open("fra_mixed_2009_10K-sentences.txt", "r", encoding="utf-8") as fichier:
for phrase in fichier:
antepenultieme, penultieme = "", ""
for word in phrase.split():
suivants[(antepenultieme, penultieme)].append(word)
antepenultieme, penultieme = penultieme, word
phrases = []
for _ in range(randrange(1, 4)):
antepenultieme, penultieme = "", ""
phrase = []
while mots_possibles := suivants[(antepenultieme, penultieme)]:
mot_suivants = choice(mots_possibles)
phrase.append(mot_suivants)
antepenultieme, penultieme = penultieme, mot_suivants
phrases.append(" ".join(phrase))
print("\n".join(phrases))

View File

@@ -3,18 +3,59 @@ header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-cache'); // pour éviter le buffering du navigateur header('Cache-Control: no-cache'); // pour éviter le buffering du navigateur
header('X-Accel-Buffering: no'); // si nginx, pour désactiver le buffering header('X-Accel-Buffering: no'); // si nginx, pour désactiver le buffering
$reponse = `python markov.py`; $suivants = [];
$total = [];
// désactiver le buffering PHP // Lire le fichier fra_mixed_2009_100K-sentences.txt
@ini_set('output_buffering', 'off'); $fichier = fopen("fra_mixed_2009_10K-sentences.txt", "r");
@ini_set('zlib.output_compression', 'off'); if ($fichier) {
while (ob_get_level()) ob_end_flush(); while (($phrase = fgets($fichier)) !== false) {
flush(); $antepenultieme = "";
$penultieme = "";
// envoyer chaque lettre avec un délai $mots = explode(" ", trim($phrase));
$len = strlen($reponse); foreach ($mots as $mot) {
for ($i = 0; $i < $len; $i++) { $cle = $antepenultieme . " " . $penultieme;
echo $reponse[$i]; if (!isset($suivants[$cle])) {
flush(); // forcer l'envoi immédiat $suivants[$cle] = [];
usleep(40000); // 40ms = 40000 microsecondes
} }
if (!isset($suivants[$cle][$mot])) {
$suivants[$cle][$mot] = 0;
}
$suivants[$cle][$mot]++;
if (!isset($total[$cle])) {
$total[$cle] = 0;
}
$total[$cle]++;
$antepenultieme = $penultieme;
$penultieme = $mot;
}
}
fclose($fichier);
} else {
http_response_code(500);
die("Impossible d'ouvrir le fichier fra_mixed_2009_10K-sentences.txt\n");
}
$phrases = [];
for ($i = 0; $i < rand(1, 3); $i++) {
$antepenultieme = "";
$penultieme = "";
$phrase = [];
while (isset($suivants[$antepenultieme . " " . $penultieme]) && !empty($suivants[$antepenultieme . " " . $penultieme])) {
$cle = $antepenultieme . " " . $penultieme;
$choix = rand(1, $total[$cle]);
foreach ($suivants[$cle] as $mot => $occurences) {
if ($choix <= $occurences) {
$suivant = $mot;
break;
}
$choix -= $occurences;
}
$phrase[] = $suivant;
$antepenultieme = $penultieme;
$penultieme = $suivant;
}
$phrases[] = implode(" ", $phrase);
}
echo implode("\n", $phrases) . "\n";