Un message vocal devient des mots datés
Vous envoyez un message vocal et une appli affiche ce que vous avez dit, avec une heure à côté de chaque partie. L’appli n’a jamais entendu de voix. On lui a donné une longue liste de nombres.
Cette leçon montre comment le son devient des nombres, comment on les découpe, et comment les mots reviennent avec leur heure.
À la fin de cette leçon
Remarque
Je peux découper des échantillons en trames et relier une transcription horodatée en lignes avec des horodatages.
Mots clés
| Mot | Ce que ça veut dire |
|---|---|
| Échantillon | une mesure du volume du son à un instant |
| Fréquence | le nombre d’échantillons pris chaque seconde |
| Trame | une courte suite d’échantillons traitée comme un morceau |
| Saut | le nombre d’échantillons entre le début d’une trame et celui de la suivante |
| Transcription | les mots dits, avec l’heure de chaque partie |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Le son est une liste d’échantillons
Un micro mesure le volume de l’air, encore et encore. Chaque mesure est un échantillon, un nombre qui peut être positif ou négatif.
La fréquence dit combien d’échantillons on prend chaque seconde. Pour la parole, on en prend souvent 16 000. Ici, on utilise 100 pour garder des listes courtes.
La longueur d’une liste d’échantillons
| Fréquence (par seconde) | Secondes | Échantillons |
|---|---|---|
| 100 | 1 | 100 |
| 100 | 3 | 300 |
| 16 000 | 1 | 16 000 |
| 16 000 | 2 | 32 000 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Les trames découpent les échantillons
Un modèle ne lit pas un échantillon à la fois. On découpe les échantillons en trames, de courtes suites de même longueur. Le saut dit à quelle distance la trame suivante commence.
Avec une trame de 10 et un saut de 10, les trames ne se recouvrent pas. Avec un saut de 5, elles se recouvrent. L’heure de début est le numéro de la trame fois le saut, divisé par la fréquence.
Une erreur courante
Attention
Beaucoup calculent le début d’une trame à partir de la taille de la trame. La trame suivante commence un saut plus loin, pas une trame plus loin. Si les trames se recouvrent, les deux nombres diffèrent. Utilisez toujours le saut.
Une transcription a des heures
Un reconnaisseur rend des parties : quelques mots, un début et une fin en millisecondes. Des parties qui se suivent de près vont sur la même ligne.
Reliez une partie à la ligne d’avant quand l’écart qui les sépare est inférieur à une limite. Écrivez ensuite chaque ligne avec un horodatage comme [00:03.0].
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script du son, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const FREQUENCE = 100;' 'const echantillons = [];' 'for (let i = 0; i < 80; i++) {' ' const amp = i >= 20 && i < 60 ? 40 : 2;' ' echantillons.push((i % 2 === 0 ? amp : -amp) + (i % 5));' '}' 'const parties = [' ' { debut: 200, fin: 900, texte: \"éteins\" },' ' { debut: 950, fin: 1400, texte: \"la lumière\" },' ' { debut: 3000, fin: 3600, texte: \"merci\" },' '];' > son4.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script du son, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function trames(s, taille, saut) {' ' const sortie = [];' ' for (let i = 0; i + taille <= s.length; i += saut) sortie.push(s.slice(i, i + taille));' ' return sortie;' '}' 'function volume(t) {' ' let somme = 0;' ' for (const v of t) somme += Math.abs(v);' ' return Math.floor(somme / t.length);' '}' >> son4.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script du son, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function horodatage(ms) {' ' const m = Math.floor(ms / 60000);' ' const s = Math.floor((ms % 60000) / 1000);' ' const d = Math.floor((ms % 1000) / 100);' ' return \"[\" + String(m).padStart(2, \"0\") + \":\" + String(s).padStart(2, \"0\") + \".\" + d + \"]\";' '}' 'function relier(liste, ecart) {' ' const lignes = [];' ' for (const p of liste) {' ' const derniere = lignes[lignes.length - 1];' ' if (derniere && p.debut - derniere.fin < ecart) { derniere.texte += \" \" + p.texte; derniere.fin = p.fin; }' ' else lignes.push({ debut: p.debut, fin: p.fin, texte: p.texte });' ' }' ' return lignes.map(function (l) { return horodatage(l.debut) + \" \" + l.texte; });' '}' >> son4.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script du son, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const ts = trames(echantillons, 10, 10);' ' const niveaux = ts.map(volume);' ' console.log(\"trames \" + ts.length + \", chacune \" + (10 * 1000 / FREQUENCE) + \" ms\");' ' console.log(\"volumes \" + niveaux.join(\" \"));' ' console.log(\"parole à partir de la trame \" + niveaux.findIndex(function (v) { return v >= 10; }));' ' const lignes = relier(parties, 300);' ' for (const l of lignes) console.log(l);' ' const q = \"Écrivez une courte légende pour un extrait avec ces lignes : \" + lignes.join(\" \");' ' const reponse = await demander([{ role: \"user\", content: q }], \"Quelqu’un demande d’éteindre la lumière, puis remercie.\", 30);' ' console.log(\"réponse : \" + reponse.texte);' '}' 'main();' >> son4.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Découper le son, relier les mots
docker exec -w /home/node/project agent node son4.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Les lignes affichées
| Ligne | Ce qu’elle montre |
|---|---|
| trames 8, chacune 100 ms | 80 échantillons en trames de 10 à 100 par seconde |
| volumes 2 2 40 40 40 40 2 2 | Les quatre trames du milieu sont fortes |
| parole à partir de la trame 2 | La trame 2 commence à 200 ms |
| [00:00.2] éteins la lumière | Deux parties jointes, leur écart étant de 50 ms |
| [00:03.0] merci | Cette partie vient 1600 ms plus tard : elle reste seule |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.