Una nota de voz pasa a texto
Envías una nota de voz y una aplicación muestra lo que dijiste, con una hora junto a cada parte. La aplicación nunca oyó una voz. Recibió una larga lista de números.
Esta lección enseña cómo el sonido se convierte en números, cómo se cortan y cómo vuelven las palabras con sus tiempos.
Al final de esta lección
Nota
Puedo cortar muestras en ventanas y unir una transcripción con tiempos en líneas con marcas.
Palabras clave
| Palabra | Qué significa |
|---|---|
| Muestra | una medida de lo fuerte que suena el sonido en un instante |
| Frecuencia | cuántas muestras se toman cada segundo |
| Ventana | una tanda corta de muestras tratada como un trozo |
| Salto | cuántas muestras después de la ventana anterior empieza la siguiente |
| Transcripción | las palabras dichas, con el tiempo de cada parte |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
El sonido es una lista de muestras
Un micrófono mide lo fuerte que suena el aire, una y otra vez. Cada medida es una muestra, un número que puede ser positivo o negativo.
La frecuencia dice cuántas muestras se toman cada segundo. El habla suele usar 16000 por segundo. Aquí se usa una frecuencia de 100 para que las listas sean cortas.
Cuánto mide una lista de muestras
| Frecuencia (por segundo) | Segundos | Muestras |
|---|---|---|
| 100 | 1 | 100 |
| 100 | 3 | 300 |
| 16000 | 1 | 16000 |
| 16000 | 2 | 32000 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Las ventanas cortan las muestras en trozos
Un modelo no lee una muestra cada vez. Las muestras se cortan en ventanas, tandas cortas de la misma longitud. El salto es lo que se separa el inicio de la siguiente ventana del de la anterior.
Con una ventana de 10 y un salto de 10, las ventanas no se solapan. Con un salto de 5, sí. El tiempo de inicio es el número de ventana por el salto, dividido por la frecuencia.
Un error frecuente
Advertencia
Mucha gente calcula el inicio de una ventana con el tamaño de la ventana. La siguiente empieza un salto después, no una ventana después. Si se solapan, los dos números difieren. Usa siempre el salto.
Una transcripción tiene tiempos
Un reconocedor devuelve partes: unas palabras, un inicio y un final en milisegundos. Las partes que van muy seguidas forman una línea.
Une una parte a la línea anterior cuando el hueco entre ellas es menor que un límite. Luego escribe cada línea con una marca como [00:03,0].
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, 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(\"estado \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script del sonido, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const RATE = 100;' 'const samples = [];' 'for (let i = 0; i < 80; i++) {' ' const amp = i >= 20 && i < 60 ? 40 : 2;' ' samples.push((i % 2 === 0 ? amp : -amp) + (i % 5));' '}' 'const parts = [' ' { start: 200, end: 900, text: \"apaga\" },' ' { start: 950, end: 1400, text: \"las luces\" },' ' { start: 3000, end: 3600, text: \"por favor\" },' '];' > sound4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script del sonido, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function frames(s, size, hop) {' ' const out = [];' ' for (let i = 0; i + size <= s.length; i += hop) out.push(s.slice(i, i + size));' ' return out;' '}' 'function loud(f) {' ' let t = 0;' ' for (const v of f) t += Math.abs(v);' ' return Math.floor(t / f.length);' '}' >> sound4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script del sonido, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function stamp(ms) {' ' const m = Math.floor(ms / 60000);' ' const s = Math.floor((ms % 60000) / 1000);' ' const t = Math.floor((ms % 1000) / 100);' ' return \"[\" + String(m).padStart(2, \"0\") + \":\" + String(s).padStart(2, \"0\") + \",\" + t + \"]\";' '}' 'function join(list, gap) {' ' const lines = [];' ' for (const p of list) {' ' const last = lines[lines.length - 1];' ' if (last && p.start - last.end < gap) { last.text += \" \" + p.text; last.end = p.end; }' ' else lines.push({ start: p.start, end: p.end, text: p.text });' ' }' ' return lines.map(function (l) { return stamp(l.start) + \" \" + l.text; });' '}' >> sound4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script del sonido, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const fs = frames(samples, 10, 10);' ' const levels = fs.map(loud);' ' console.log(\"ventanas \" + fs.length + \", cada una de \" + (10 * 1000 / RATE) + \" ms\");' ' console.log(\"volumen \" + levels.join(\" \"));' ' console.log(\"voz desde la ventana \" + levels.findIndex(function (v) { return v >= 10; }));' ' const lines = join(parts, 300);' ' for (const l of lines) console.log(l);' ' const q = \"Escribe un pie corto para un clip con estas líneas: \" + lines.join(\" \");' ' const reply = await ask([{ role: \"user\", content: q }], \"Alguien pide que apaguen las luces.\", 30);' ' console.log(\"respuesta: \" + reply.text);' '}' 'main();' >> sound4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Corta el sonido y une las palabras
docker exec -w /home/node/project agent node sound4.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Las líneas que imprime
| Línea | Qué muestra |
|---|---|
| ventanas 8, cada una de 100 ms | 80 muestras en ventanas de 10 a 100 por segundo |
| volumen 2 2 40 40 40 40 2 2 | Las cuatro ventanas del medio son fuertes |
| voz desde la ventana 2 | La ventana 2 empieza en 200 ms |
| [00:00,2] apaga las luces | Dos partes unidas, porque su hueco era de 50 ms |
| [00:03,0] por favor | Esta parte llegó 1600 ms después, así que va sola |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.