Главная → Блог → Распознавание речи в Node.js: рабочий код и привычные грабли
Распознавание речи в Node.js: рабочий код и привычные грабли
В Node.js сам вызов расшифровки тривиален. Спотыкаются на том, что вокруг: файловые дескрипторы против буферов, загрузки, приходящие из браузера, и таймаут по умолчанию, который тихо убивает длинные записи.
Установка и первый запрос
Официальный SDK работает без изменений — отличаются только базовый адрес и ключ:
npm install openai
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://voicesscribe.com/v1",
apiKey: process.env.VS_KEY,
});
const r = await client.audio.transcriptions.create({
model: "whisper-1",
file: fs.createReadStream("audio.mp3"),
});
console.log(r.text);
Обратите внимание на createReadStream, а не readFileSync: файл уходит на сервер потоком, а не грузится сначала целиком в память. На сервере, обрабатывающем несколько запросов сразу, эта разница и есть весь профиль потребления памяти.
Аудио, пришедшее из браузера
Типичный случай — не файл на диске, а загрузка. С Express и multer буфер нужно обернуть, чтобы SDK увидел файл с именем:
import express from "express";
import multer from "multer";
import { toFile } from "openai/uploads";
const upload = multer({ limits: { fileSize: 25 * 1024 * 1024 } });
const app = express();
app.post("/transcribe", upload.single("audio"), async (req, res) => {
try {
const file = await toFile(req.file.buffer, req.file.originalname);
const r = await client.audio.transcriptions.create({
model: "whisper-1", file,
});
res.json({ text: r.text });
} catch (e) {
res.status(502).json({ error: String(e) });
}
});
Ограничение fileSize у multer важно: без него браузер способен затолкать гигабайт в память вашего процесса раньше, чем что-либо это отклонит. Лимит API — 25 МБ, и совпадение с ним выше по стеку даёт быстрый и дешёвый отказ.
Запись в браузере и отправка на сервер
Браузерная сторона отдаёт webm из MediaRecorder — он принимается как есть, конвертация не нужна:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const rec = new MediaRecorder(stream);
const chunks = [];
rec.ondataavailable = (e) => chunks.push(e.data);
rec.onstop = async () => {
const blob = new Blob(chunks, { type: "audio/webm" });
const form = new FormData();
form.append("audio", blob, "voice.webm");
const res = await fetch("/transcribe", { method: "POST", body: form });
console.log((await res.json()).text);
};
rec.start();
setTimeout(() => rec.stop(), 5000);
Никогда не кладите ключ API в браузерный код. Загрузка идёт на ваш собственный эндпоинт, а ключ остаётся на сервере — это единственная безопасная схема.
Таймауты и повторные попытки
Таймаут запроса по умолчанию рассчитан на короткие фрагменты и молча убивает длинные. Ставьте его от той длительности, с которой вы реально работаете, а обработку временных сбоев отдайте SDK:
const client = new OpenAI({
baseURL: "https://voicesscribe.com/v1",
apiKey: process.env.VS_KEY,
timeout: 5 * 60 * 1000, // пять минут: хватает на часовую запись
maxRetries: 2, // 5xx и ошибки соединения, обрабатывает SDK
});
Повторы имеют смысл для 5xx и ошибок соединения. Ошибка 400 на битом файле повторится одинаково, поэтому её повтор — только потерянное время.
Форматы и таймкоды
Субтитры и время сегментов приходят из того же вызова, разница в одном параметре:
// готовый файл субтитров строкой
const srt = await client.audio.transcriptions.create({
model: "whisper-1", file, response_format: "srt",
});
// сегменты с таймкодами
const detailed = await client.audio.transcriptions.create({
model: "whisper-1", file, response_format: "verbose_json",
});
for (const s of detailed.segments) {
console.log(s.start.toFixed(1), s.text.trim());
}Ошибки, которые стоит обработать явно
| Код | Что означает | Что делать |
|---|---|---|
| 401 | Ключ неверен или заблокирован | Проверьте, что переменная окружения дошла до процесса |
| 413 | Файл больше 25 МБ | Пережать в моно 16 кГц, при необходимости разрезать |
| 429 | Лимит запросов или закончились минуты | Растущая пауза; не повторять сразу |
| 502 | Узел распознавания недоступен | Повторить после паузы |
Пустое поле text в успешном ответе — не ошибка: в записи не было речи. Обрабатывайте это как обычный исход и не повторяйте запрос.
Попробуйте на своих записях. Регистрация занимает минуту, бесплатных минут хватает, чтобы оценить качество.
Получить ключ бесплатноЧастые вопросы
Нужен ли специальный клиент для Node.js?
Нет, официальный пакет openai работает без изменений. API совместим по протоколу, поэтому от настроек OpenAI отличаются только baseURL и apiKey.
Как передать загруженный файл из Express?
Оберните буфер функцией toFile из openai/uploads, чтобы SDK увидел полноценный файл с именем, и передайте его в поле file.
Можно ли вызывать API прямо из браузера?
Нет — это откроет ваш ключ каждому, кто откроет страницу. Загружайте на свой эндпоинт, а ключ держите на сервере.
Принимается ли webm из MediaRecorder?
Да, webm принимается как есть, наравне с ogg, opus, mp3, wav и m4a. Отдельный шаг конвертации не нужен.
Почему длинные записи падают по таймауту?
Значение клиента по умолчанию настроено на короткие запросы. Поднимите опцию timeout под самую длинную запись, с которой работаете: пяти минут с запасом хватает на часовую.
Читайте также
- Распознавание речи на Python: от одного файла до рабочего скрипта — Рабочая расшифровка аудио на Python за десять минут: установка, первый запрос, форматы ответа, обработка ошибок и приёмы, которые экономят больше всего времени.
- Транскрибация голосовых сообщений: Telegram, WhatsApp и другие мессенджеры — Как автоматически переводить голосовые сообщения в текст: работа с форматом ogg/opus, обработка коротких записей, примеры кода для чат-ботов.
- Как построить пакетную транскрибацию архива записей — Как расшифровать тысячи записей и ничего не потерять: очередь работ, параллельность, повторные попытки, продолжение после сбоя и контроль расходов до запуска.