NexaraNexara

TypeScript SDK

Официальная TypeScript/JavaScript-библиотека Nexara: транскрибация, диаризация, роли, эмоции и LLM-анализ в несколько строк кода.

nexara-sdk — официальная TypeScript/JavaScript-библиотека для Nexara API. Она берёт на себя сборку запросов, разбор ответов в типизированные объекты, обработку ошибок и повторные попытки, а асинхронные задачи превращает в один вызов job.wait(). Библиотека полностью типизирована и поставляет собственные .d.ts, тип результата сужается автоматически по task и response_format. Требуется Node.js 20+.

npm install nexara-sdk

Сохраните API-ключ в переменной окружения NEXARA_API_KEY. SDK читает её автоматически при создании клиента, поэтому ключ не нужно указывать в коде — и он не попадёт в репозиторий:

export NEXARA_API_KEY=nx-...

Команда export задаёт переменную только для текущей сессии терминала. Чтобы ключ сохранялся между сессиями, добавьте эту строку в файл конфигурации вашей оболочки (например, ~/.zshrc или ~/.bashrc). На сервере переменную удобнее задавать через настройки окружения или менеджер секретов.

Быстрый старт

import { Nexara } from "nexara-sdk";

const client = new Nexara(); // ключ из переменной окружения NEXARA_API_KEY

const { text } = await client.transcriptions.create({ file: "audio.mp3" });
console.log(text);

Клиент берёт ключ из NEXARA_API_KEY — той самой переменной, что вы задали выше, — поэтому в коде его указывать не нужно. Передать ключ явным полем apiKey тоже можно, но не храните его в коде и не коммитьте в репозиторий.

Клиент

const client = new Nexara({
  // apiKey по умолчанию берётся из переменной окружения NEXARA_API_KEY,
  // либо можете передать ключ сами: apiKey: "nx-..."
  timeoutMs: 600_000, // таймаут запроса в миллисекундах
  maxRetries: 2, // повторы при 429 и сетевых сбоях
});

Клиент асинхронный целиком — отдельного синхронного варианта нет (в отличие от Nexara/AsyncNexara в Python SDK). Все методы возвращают промисы.

Транскрибация

Передайте ровно один из параметров: file (путь, Uint8Array или Blob) или url (прямая ссылка). Файл, переданный путём, стримится с диска и не загружается в память целиком.

// Локальный файл
const local = await client.transcriptions.create({ file: "audio.mp3" });
console.log(local.text);

// Файл по ссылке
const remote = await client.transcriptions.create({
  url: "https://example.com/audio.mp3",
});
console.log(remote.text);

Параметры те же, что и у API: model, language, response_format, timestamp_granularities, profanity_filter и другие. Тип результата сужается автоматически: text, srt и vtt возвращаются строкой, json и verbose_json — типизированными объектами:

import { writeFile } from "node:fs/promises";

const srt = await client.transcriptions.create({
  file: "video.mp4",
  response_format: "srt",
});
await writeFile("video.srt", srt);

const verbose = await client.transcriptions.create({
  file: "audio.mp3",
  response_format: "verbose_json",
  timestamp_granularities: ["word"],
});
for (const word of verbose.words ?? []) {
  console.log(`${word.start.toFixed(2)}–${word.end.toFixed(2)}: ${word.word}`);
}

Диаризация и роли

Разделение по говорящим включается так же, как в API, — task: "diarize". Роли передаются обычным массивом или объектом: SDK сам сериализует их в JSON.

const call = await client.transcriptions.create({
  file: "call.mp3",
  task: "diarize",
  num_speakers: 2,
  roles: ["Оператор", "Клиент"], // или "auto", или { "Роль": "описание" }
});

for (const segment of call.segments) {
  console.log(`${segment.speaker}: ${segment.text}`);
}

Подробнее о режимах — на странице спикеры и роли.

Эмоции

Флаг emotions: true добавляет к каждому сегменту диаризации объект emotion: метку label (angry, sad, neutral или positive), уверенность модели confidence и распределение вероятностей probs.

const call = await client.transcriptions.create({
  file: "call.mp3",
  task: "diarize",
  model: "nexara-ru",
  emotions: true,
});

for (const segment of call.segments) {
  if (segment.emotion) {
    const confidence = Math.round(segment.emotion.confidence * 100);
    console.log(`${segment.speaker}: ${segment.emotion.label} (${confidence}%) — ${segment.text}`);
  } else {
    console.log(`${segment.speaker}: эмоция не определена — ${segment.text}`);
  }
}

Эмоция приходит не для каждого сегмента: если модель не смогла оценить фрагмент, поле emotion будет undefined. Проверяйте его перед чтением, как в примере выше. У слов (words) эмоции нет — она считается по сегменту целиком.

Метки доступны типом EmotionLabel, а сам объект — типом Emotion:

import type { Emotion, EmotionLabel } from "nexara-sdk";

Подробнее об условиях и ограничениях — на странице распознавание эмоций.

Распознавание эмоций тарифицируется дополнительно к стоимости диаризации. Если ни один сегмент не удалось оценить, надбавка не списывается. Стоимость услуги смотрите на странице тарифов.

Асинхронные задачи

Для длинных записей используйте асинхронный режим: createJob() ставит задачу и сразу возвращает объект Job, а job.wait() опрашивает статус и возвращает готовый результат.

const job = await client.transcriptions.createJob({
  file: "long_recording.mp3",
});
console.log(job.job_id, job.status); // in_progress

const result = await job.wait(); // опрашивает статус; таймаут по умолчанию — 1_800_000 мс
console.log((result as { text: string }).text);

Задачу можно забрать позже — в том числе из другого процесса:

const job = await client.transcriptions.retrieveJob(jobId);
if (job.status === "complete") {
  console.log(job.result);
}

Что важно знать:

  • Результат хранится 12 часов с момента создания задачи, затем удаляется — после этого retrieveJob() бросит NotFoundError.
  • Одновременно на один ключ может выполняться до 200 задач.
  • Если задача завершилась ошибкой, wait() бросает JobFailedError. Неудавшаяся задача не тарифицируется — повторная отправка бесплатна.
  • Если задача не успела за таймаут, wait() бросает JobTimeoutError; сама задача при этом не отменяется, её можно забрать позже через retrieveJob().

LLM-анализ

Передайте prompt, чтобы прогнать расшифровку через языковую модель, и json_schema — чтобы получить структурированный ответ. Схема передаётся обычным объектом, а llm_output возвращается уже разобранным — никакого ручного JSON.parse.

const result = await client.transcriptions.create({
  file: "meeting.mp3",
  prompt: "Составь протокол встречи: резюме, решения и задачи.",
  json_schema: {
    type: "object",
    properties: {
      summary: { type: "string" },
      decisions: { type: "array", items: { type: "string" } },
      action_items: { type: "array", items: { type: "string" } },
    },
    required: ["summary", "decisions", "action_items"],
  },
});

console.log(result.llm_output); // объект по вашей схеме
console.log(result.transcription.text); // расшифровка, из которой он получен

Без json_schema поле llm_output — строка со свободным ответом модели.

Параллельные запросы

Клиент асинхронный целиком — «async» здесь означает промисы. Отдельного синхронного клиента нет, поэтому несколько запросов запускаются обычным Promise.all:

const urls = [
  "https://example.com/a.mp3",
  "https://example.com/b.mp3",
  "https://example.com/c.mp3",
];

const results = await Promise.all(
  urls.map((url) => client.transcriptions.create({ url })),
);

for (const result of results) {
  console.log(result.text);
}

Realtime

Распознавание в реальном времени: connect() открывает WebSocket-сессию и резолвится, когда сервер прислал session.created; stream() отправляет аудио из любого асинхронного итерируемого и отдаёт события RealtimeTranscript по мере распознавания:

const session = await client.realtime.connect({ sampleRate: 16000, diarize: true });
for await (const event of session.stream(microphone())) { // AsyncIterable<Uint8Array>
  console.log(`spk${event.speaker} ${(event.words[0]!.start / 1000).toFixed(2)}s ${event.text}`);
}
console.log(session.ended?.text); // полная транскрипция и billed_ms

Если аудио приходит не как итератор, отправляйте его сами и завершите сессию через finish() — он посылает input_audio.end и резолвится с session.ended. События также доступны через on("transcript"), on("ended"), on("error") и on("close"):

const session = await client.realtime.connect();
session.on("transcript", (event) => process.stdout.write(event.text));
for await (const chunk of microphone()) await session.sendAudio(chunk);
const { billed_ms } = await session.finish();

Параметры connect(): encoding, sampleRate, channels, multichannel, diarize, delayMs, turnDetection, clientId — те же, что у протокола; недопустимые значения отклоняются NexaraValidationError до подключения.

Для голосовых агентов есть определение реплик: с turnDetection: "smart" сессия кроме RealtimeTranscript отдаёт события RealtimeSpeechStarted, RealtimeSpeechEnded и RealtimeTurnEnd — итератор типизирован объединением RealtimeEvent, различайте по event.type (те же события доступны через on("speech.started"), on("speech.ended"), on("turn.end")), а у каждого transcript появляется номер реплики turn:

const session = await client.realtime.connect({ turnDetection: "smart" });
for await (const event of session.stream(microphone())) {
  switch (event.type) {
    case "speech.started": agent.interrupt(); break;   // ~100 мс после начала речи
    case "turn.end": await llm(event.text); break;     // полный текст реплики — в языковую модель
    case "transcript": console.log(event.turn, event.text);
  }
}

turn.end приходит после всех transcript своей реплики и до первого transcript следующей, так что event.text полон; event.confidence — вероятность конца реплики (null, если реплику закрыл finish()).

Ключ передаётся субпротоколом WebSocket, поэтому подходит любой стандартный WebSocket: в браузерах и Node.js 22+ он встроен, на Node.js 20 передайте класс из пакета ws — connect({ WebSocket }) или один раз в new Nexara({ WebSocket }).

Если сервер завершает сессию с ошибкой, итератор выдает RealtimeError с полями code и reason (session_full, idle_timeout, audio_backlog, service_restart, …) или InsufficientBalanceError, если баланс закончился посреди сессии. Отказ на этапе подключения стандартный WebSocket показывает без HTTP-статуса — это APIConnectionError; с пакетом ws он превращается в AuthenticationError / InsufficientBalanceError / RateLimitError, как в REST. Полное описание протокола — на странице распознавания в реальном времени.

Ошибки

Некорректные запросы (например, file и url одновременно, слишком много ролей) отклоняются на стороне клиента с NexaraValidationError — ещё до обращения к серверу. Ошибки сервера превращаются в типизированные классы по коду статуса:

КлассКодКогда возникает
BadRequestError400Некорректные параметры запроса
InsufficientBalanceError402Недостаточно средств на балансе
AuthenticationError403Ключ не передан или недействителен
NotFoundError404Задача, модель или ключ не найдены
RateLimitError429Превышен лимит частоты или число одновременных задач
InternalServerError500Внутренняя ошибка сервера
RealtimeError—Сервер завершил realtime-сессию; код и причина в code / reason
APIConnectionError—Запрос не дошёл до сервера (сеть, таймаут)

Все они наследуются от NexaraError, а у ошибок API есть поля status_code и detail с сообщением сервера. Тип ошибки проверяется через instanceof. Полный список кодов — на странице ошибок.

import { Nexara, InsufficientBalanceError, RateLimitError } from "nexara-sdk";

const client = new Nexara();

try {
  await client.transcriptions.create({ file: "audio.mp3" });
} catch (e) {
  if (e instanceof InsufficientBalanceError) {
    console.log("Пополните баланс:", e.detail); // 402
  } else if (e instanceof RateLimitError) {
    console.log("Слишком много запросов, попробуйте позже.");
  } else {
    throw e;
  }
}

Ссылки

On this page