NexaraNexara

Распознавание эмоций

Как определить эмоции говорящих в записи: метки, уверенность модели и распределение вероятностей.

Распознавание эмоций оценивает, с какой эмоцией произнесён каждый фрагмент речи. К сегментам диаризации добавляется объект emotion с меткой (angry, sad, neutral, positive), уверенностью модели и распределением вероятностей по всем меткам.

Это удобно для аналитики звонков: видно не только что сказал клиент, но и как — например, в какой момент разговора он начал злиться.

Эмоцию определяет сама модель распознавания речи nexara-ru — отдельного запроса и отдельной обработки не требуется, всё приходит в одном ответе.

Что нужно для работы

Эмоции считаются по сегментам речи, а сегменты появляются только при диаризации, поэтому у параметра три условия:

УсловиеЗначение
Режимtask=diarize
Модельmodel=nexara-ru
Формат ответаjson или verbose_json

Форматы text, srt и vtt не подойдут: в них нет структуры, куда положить объект emotion. Любую другую комбинацию API отклоняет с ошибкой 400, а Python и TypeScript SDK — ещё до отправки файла, чтобы запрос не тарифицировался впустую.

Как включить

Передайте emotions=true в запросе на POST /v1/audio/transcriptions. Замените nx-XXXXXXXXXXXXXXXXXXXXXXXX на свой API-ключ, а call.mp3 — на путь к вашему файлу.

Официальная библиотека nexara (pip install nexara):

from nexara import Nexara

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

result = client.transcriptions.create(
    file="call.mp3",
    model="nexara-ru",
    task="diarize",
    emotions=True,
)

for segment in result.segments:
    if segment.emotion:
        print(f"{segment.speaker} [{segment.emotion.label}]: {segment.text}")
    else:
        print(f"{segment.speaker}: {segment.text}")

Официальная библиотека nexara-sdk (npm install nexara-sdk):

import { Nexara } from "nexara-sdk";

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

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

for (const segment of result.segments) {
  if (segment.emotion) {
    console.log(`${segment.speaker} [${segment.emotion.label}]: ${segment.text}`);
  } else {
    console.log(`${segment.speaker}: ${segment.text}`);
  }
}
curl https://api.nexara.ru/v1/audio/transcriptions \
  -H "Authorization: Bearer nx-XXXXXXXXXXXXXXXXXXXXXXXX" \
  -F "file=@call.mp3" \
  -F "model=nexara-ru" \
  -F "task=diarize" \
  -F "emotions=true"
import requests

with open("call.mp3", "rb") as audio_file:
    response = requests.post(
        "https://api.nexara.ru/v1/audio/transcriptions",
        headers={"Authorization": "Bearer nx-XXXXXXXXXXXXXXXXXXXXXXXX"},
        files={"file": audio_file},
        data={
            "model": "nexara-ru",
            "task": "diarize",
            "emotions": "true",
        },
    )

for segment in response.json()["segments"]:
    emotion = segment.get("emotion")
    label = emotion["label"] if emotion else "—"
    print(f"{segment['speaker']} [{label}]: {segment['text']}")

Node.js 18+:

import fs from "fs";

const form = new FormData();
form.append("file", new Blob([fs.readFileSync("call.mp3")]), "call.mp3");
form.append("model", "nexara-ru");
form.append("task", "diarize");
form.append("emotions", "true");

const response = await fetch(
  "https://api.nexara.ru/v1/audio/transcriptions",
  {
    method: "POST",
    headers: { Authorization: "Bearer nx-XXXXXXXXXXXXXXXXXXXXXXXX" },
    body: form,
  },
);

const result = await response.json();
for (const segment of result.segments) {
  console.log(`${segment.speaker} [${segment.emotion?.label ?? "—"}]: ${segment.text}`);
}
package main

import (
	"bytes"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

func main() {
	file, _ := os.Open("call.mp3")
	defer file.Close()

	var buf bytes.Buffer
	writer := multipart.NewWriter(&buf)
	part, _ := writer.CreateFormFile("file", "call.mp3")
	io.Copy(part, file)
	writer.WriteField("model", "nexara-ru")
	writer.WriteField("task", "diarize")
	writer.WriteField("emotions", "true")
	writer.Close()

	req, _ := http.NewRequest("POST", "https://api.nexara.ru/v1/audio/transcriptions", &buf)
	req.Header.Set("Authorization", "Bearer nx-XXXXXXXXXXXXXXXXXXXXXXXX")
	req.Header.Set("Content-Type", writer.FormDataContentType())

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
}
require "net/http"
require "uri"
require "json"

uri = URI("https://api.nexara.ru/v1/audio/transcriptions")

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer nx-XXXXXXXXXXXXXXXXXXXXXXXX"
request.set_form(
  [
    ["file", File.open("call.mp3")],
    ["model", "nexara-ru"],
    ["task", "diarize"],
    ["emotions", "true"],
  ],
  "multipart/form-data",
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

JSON.parse(response.body)["segments"].each do |segment|
  label = segment["emotion"] ? segment["emotion"]["label"] : "—"
  puts "#{segment["speaker"]} [#{label}]: #{segment["text"]}"
end

Результат

У каждого оценённого сегмента появляется объект emotion:

{
  "task": "diarize",
  "language": "ru",
  "duration": 12.4,
  "text": "Здравствуйте, чем могу помочь? Я третий раз звоню по одному и тому же вопросу!",
  "segments": [
    {
      "start": 0.0,
      "end": 3.21,
      "text": "Здравствуйте, чем могу помочь?",
      "speaker": "speaker_0",
      "emotion": {
        "label": "neutral",
        "confidence": 0.87,
        "probs": { "angry": 0.03, "sad": 0.04, "neutral": 0.87, "positive": 0.06 }
      }
    },
    {
      "start": 3.6,
      "end": 8.9,
      "text": "Я третий раз звоню по одному и тому же вопросу!",
      "speaker": "speaker_1",
      "emotion": {
        "label": "angry",
        "confidence": 0.91,
        "probs": { "angry": 0.91, "sad": 0.05, "neutral": 0.03, "positive": 0.01 }
      }
    }
  ]
}

Метки

МеткаЗначение
angryЗлость, раздражение
sadГрусть, расстроенность
neutralНейтральная, спокойная речь
positiveПозитив, радость, доброжелательность

Поля объекта

  • label — выбранная метка эмоции.
  • confidence — уверенность модели в этой метке, от 0 до 1.
  • probs — вероятности по всем меткам. Полезно, когда важна не только победившая эмоция: например, angry c confidence 0.45 и neutral 0.4 рядом — это скорее «лёгкое раздражение», чем настоящая злость.

Ограничения

  • Эмоция приходит не для каждого сегмента. Если модель не смогла оценить фрагмент (слишком короткий, тишина, шум), поля emotion в сегменте просто не будет. Всегда проверяйте его наличие, а не обращайтесь к нему напрямую.
  • У слов эмоции нет. Она считается по сегменту целиком, поэтому в массиве words объекта emotion не будет даже при timestamp_granularities=["word"].
  • Только nexara-ru и только диаризация. С whisper-1 или с task=transcribe параметр вернёт ошибку 400.
  • Только JSON-форматы ответа. Для text, srt и vtt параметр вернёт ошибку 400.

Эмоции в LLM-анализе

Если в том же запросе передан prompt, метки эмоций уходят в языковую модель вместе с расшифровкой — анализ тональности опирается не только на слова, но и на то, как они сказаны. Модель видит примерно такой текст:

Оператор [neutral]: Служба поддержки, здравствуйте.
Клиент [angry]: Я третий раз звоню по одному и тому же вопросу!

Ничего дополнительно включать не нужно: достаточно передать emotions=true и prompt в одном запросе. Подробнее про промпты и схемы — на странице LLM-анализ.

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

  • В модель уходит только метка. confidence и probs остаются в ответе API, но в промпт не передаются.
  • Метка акустическая. Она получена из тона голоса, а не из слов, поэтому может расходиться со смыслом сказанного — вежливая фраза, произнесённая раздражённо, получит метку angry. Модель об этом предупреждена отдельно.

Без emotions=true промпт не меняется: расшифровка уходит в модель ровно в том же виде, что и раньше.

Длинные записи

Для длинных файлов параметр работает так же в асинхронном режиме — передайте emotions=true при постановке задачи, и эмоции придут в готовом результате.

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

On this page