Распознавание эмоций
Как определить эмоции говорящих в записи: метки, уверенность модели и распределение вероятностей.
Распознавание эмоций оценивает, с какой эмоцией произнесён каждый фрагмент речи. К сегментам диаризации добавляется объект 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— вероятности по всем меткам. Полезно, когда важна не только победившая эмоция: например,angrycconfidence0.45 иneutral0.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 при постановке задачи, и эмоции придут в готовом результате.
Распознавание эмоций тарифицируется дополнительно к стоимости диаризации. Если ни один сегмент не удалось оценить, надбавка не списывается. Стоимость услуги смотрите на странице тарифов.