# MultiVoiceInput — сбор нескольких голосовых сообщений в одну коллекцию

**Пакет:** `Voice` 
**Полное имя компонента:** `Common.Voice.MultiVoiceInput`
**Текущая версия в приложенном срезе:** `0.1`
**Базовый компонент для понимания:** `VoiceInput`

Рекомендуем ознакомиться с базовым уроком по голосовому вводу: [VoiceInput](https://docs.metabot24.ru/books/06-ai-komponenty-metabot-agent-stack/page/voice-input-golosovoi-interfeis-dlya-ai-voronok).

---

## Что это

`MultiVoiceInput` — это высокоуровневый компонент Metabot для сбора **нескольких голосовых сообщений подряд** внутри сценария.

Он нужен, когда пользователю неудобно или неестественно укладывать весь ответ в одно голосовое.

Например, человек начал рассказывать задачу, потом вспомнил важную деталь, потом отправил ещё одно голосовое, потом ещё одно. Обычный `VoiceInput` ждёт одно сообщение и после распознавания сразу переводит сценарий дальше. `MultiVoiceInput` работает иначе: он собирает несколько голосовых в одну коллекцию, распознаёт каждое, сохраняет общий текст и завершает сбор только после команды вроде “готово”.

Базовый паттерн компонента в исходнике описан так: `collect()` ждёт `voice / video_note / audio`, отправляет файл в STT, добавляет результат в `collectionAttr`, обновляет `fullTextAttr`, ждёт следующее голосовое, а фраза “готово” завершает сбор и переводит сценарий в `successScript`. 

Проще говоря:

```text
VoiceInput = одно голосовое → один текст → следующий сценарий

MultiVoiceInput = несколько голосовых → коллекция текстов → общий full_text → следующий сценарий
```

---

## Когда использовать MultiVoiceInput

`MultiVoiceInput` нужен там, где пользователь должен рассказать что-то свободно и развёрнуто, но может делать это частями.

Типовые кейсы:

```text
AI Intake;
голосовая диагностика бизнеса;
бриф на разработку;
сбор обратной связи;
интервью эксперта;
профилирование пользователя;
разбор проблемы в поддержке;
онбординг партнёра;
подготовка к LLM-анализу;
сбор контекста перед routing / CRM / оффером.
```

Например, в AI Intake мы просим пользователя рассказать, что он хочет улучшить через AI, какие системы уже есть, где теряются деньги, время, заявки или знания. Человек может отправить 2–5 голосовых подряд, а уже после этого сценарий отправит общий текст в `LLMQuery`.

---

## Чем MultiVoiceInput отличается от VoiceInput

`VoiceInput` — это базовый компонент голосового ввода. Он принимает одно голосовое сообщение, распознаёт его и сохраняет результат в один атрибут. Подробнее про философию, пользу, pipeline, настройки канала и базовый processor script смотрите в основной статье:

[VoiceInput](https://docs.metabot24.ru/books/06-ai-komponenty-metabot-agent-stack/page/voice-input-golosovoi-interfeis-dlya-ai-voronok)

`MultiVoiceInput` не заменяет этот урок. Он продолжает ту же архитектурную идею.

Разница такая:

```text
VoiceInput:
- один пользовательский голосовой ответ;
- один targetAttr;
- один sourceAttr;
- после STT сразу successScript.

MultiVoiceInput:
- несколько голосовых подряд;
- collectionAttr со списком всех элементов;
- fullTextAttr с общим текстом;
- sourceUrlsAttr со списком источников;
- countAttr с количеством голосовых;
- statusAttr со статусом сбора;
- завершение по finish phrase, лимиту maxItems или лимиту длительности.
```

То есть `MultiVoiceInput` — это не просто “ещё один STT”. Это компонент для **голосовой сессии**, где пользователь может дать контекст кусками.

---

## Общая схема работы

```text
Сценарий
→ MultiVoiceInput.collect()
→ ожидание voice / video_note / audio
→ callback от мессенджера
→ получение файла
→ отправка в STT
→ callback от STT
→ добавление item в коллекцию
→ обновление full_text
→ ожидание следующего голосового
→ пользователь пишет “готово”
→ сбор завершается
→ переход в successScript
```

В рабочем AI Intake reference project компонент используется именно так: сценарий берёт текущий номер раунда, запускает сбор 1–5 голосовых, сохраняет транскрипты в round-specific attrs и после завершения переводит пользователя в сценарий LLM-анализа. 

---

## Где находится компонент

Подключение:

```javascript
const MultiVoiceInput = require("Common.Voice.MultiVoiceInput")
```

Запуск:

```javascript
return MultiVoiceInput.collect({...})
```

Есть alias:

```javascript
MultiVoiceInput.start({...})
```

В текущем исходнике `start()` является алиасом на `collect()`. 

---

## Что нужно настроить перед использованием

Перед использованием `MultiVoiceInput` должны быть готовы те же базовые вещи, что и для `VoiceInput`.

### 1. Настройки канала

Для Telegram-контура проверьте, что канал обрабатывает голосовые штатно:

```text
Реакция на аудио:
Штатная (NLP и меню)

Реакция на голосовые сообщения:
Штатная (NLP и меню)
```

Это базовое требование голосового ввода: если голосовые не попадают в сценарий, компонент не сможет их обработать. Основная статья по `VoiceInput` отдельно описывает эту настройку канала. 

### 2. Processor script

Нужен системный сценарий, обычно:

```text
MultiVoiceInput_Processor
```

Он должен содержать две команды.

### 3. Bot attrs

Минимально нужны:

```text
METABOT_API_TOKEN
METABOT_SERVER_DOMAIN
OPENAI_API_KEY
```

`OPENAI_API_KEY` используется через `tokenKey` в STT-настройках. `METABOT_API_TOKEN` и `METABOT_SERVER_DOMAIN` нужны для callback-механики, как и в обычном `VoiceInput`.

---

## Processor script

`MultiVoiceInput` работает через системный processor script. Он нужен потому, что компонент проходит несколько фаз: сначала ждёт сообщение от пользователя, потом получает файл, потом отправляет его в STT, потом ждёт async callback с текстом.

Processor script должен содержать две команды.

### Команда 1. Callback от мессенджера

Тип команды:

```text
Run JavaScript Callback
```

Код:

```javascript
const MultiVoiceInput = require("Common.Voice.MultiVoiceInput")

return MultiVoiceInput.onCallback({ lead, isFirstImmediateCall })
```

Эта команда:

```text
показывает wait-сообщение;
ждёт голосовое / video_note / audio;
проверяет finish phrases;
проверяет stop phrases;
проверяет help phrases;
проверяет add more phrases;
валидирует файл;
готовит текущий файл для STT.
```

### Команда 2. Callback от STT

Тип команды:

```text
Run asynchronous API-request
```

Код:

```javascript
const MultiVoiceInput = require("Common.Voice.MultiVoiceInput")

return MultiVoiceInput.onSTT({ lead, isFirstImmediateCall })
```

Эта команда:

```text
инициирует STT;
ждёт async response;
получает распознанный текст;
добавляет item в collection;
обновляет fullTextAttr;
обновляет countAttr;
если лимит не достигнут — возвращает пользователя к ожиданию следующего голосового;
если лимит достигнут — завершает сбор.
```

В reference project `MultiVoiceInput_Processor` устроен именно так: первая команда вызывает `onCallback`, вторая — `onSTT`. 

---

## Пример использования

```javascript
const MultiVoiceInput = require("Common.Voice.MultiVoiceInput");

return MultiVoiceInput.collect({
  code: "demand_voice_diagnostic_intake",

  lead,

  successScript: "demand_voice_intake1_analyze",
  cancelScript: "demand_voice_diagnostic_cancelled",
  errorScript: "demand_voice_diagnostic_voice_error",

  collectionAttr: "demand_voice_intake1_items_json",
  fullTextAttr: "demand_voice_intake1_full_text",
  sourceUrlsAttr: "demand_voice_intake1_source_urls_json",
  countAttr: "demand_voice_intake1_count",
  statusAttr: "demand_voice_intake1_status",

  minItems: 1,
  maxItems: 5,
  maxTotalDurationSec: 900,

  extraAttrs: {
    active_agent: "metabot_intake",
    voice_context: "demand_voice_diagnostic",
    input_mode: "multi_voice_intake"
  },

  processorScript: "MultiVoiceInput_Processor",

  stt: {
    provider: "openai",
    options: {
      model: "whisper-1",
      language: "ru"
    },
    asyncResponse: true,
    tokenKey: "OPENAI_API_KEY"
  },

  constraints: {
    allow: {
      voice: true,
      video_note: true,
      audio: false
    },
    minDurationSec: 5,
    maxDurationSec: 600,
    maxFileSizeBytes: 20 * 1024 * 1024
  },

  messages: {
    wait: "🎙 Пришлите голосовое сообщение. Можно несколько подряд.",
    accepted: "✅ Принял голосовое. Расшифровываю…",
    afterItem: "Принял. Можете прислать ещё одно голосовое или написать «готово».",
    wrong: "Нужна голосовуха. Если закончили — напишите «готово».",
    empty: "Пока нет ни одного голосового. Пришлите хотя бы одно сообщение.",
    canceled: "Ок, остановились.",
    finished: "Спасибо. Собрал контекст.",
    tooMany: "Я уже собрал достаточно материала. Сейчас соберу картину.",
    stillProcessing: "⏳ Ещё обрабатываю предыдущее голосовое…"
  },

  finishPhrases: [
    "готово",
    "всё",
    "все",
    "закончил",
    "закончила",
    "/done"
  ],

  stopPhrases: [
    "стоп",
    "stop",
    "отмена",
    "cancel",
    "я передумал",
    "/cancel"
  ]
});
```

В текущем исходнике обязательными являются `processorScript`, `successScript`, `cancelScript`, `collectionAttr`, `fullTextAttr` и `stt.tokenKey`; также проверяется, что `minItems >= 1`, а `maxItems >= minItems`. 

---

## Важное замечание по свежести примеров

В некоторых переходных примерах может встречаться вложенный блок:

```javascript
voiceInput: {
  processorScript: "MultiVoiceInput_Processor",
  stt: {...},
  constraints: {...}
}
```

Но в текущем срезе компонента `Common.Voice.MultiVoiceInput` основные настройки `processorScript`, `stt` и `constraints` находятся на верхнем уровне конфигурации. Поэтому при новом внедрении сверяйте вызов с актуальной версией плагина на платформе.

Для production используйте свежую версию компонента из платформы, а приложенный reference project воспринимайте как рабочий срез на дату.

---

## Что сохраняет компонент

`MultiVoiceInput` сохраняет несколько типов результата.

### collectionAttr

JSON-массив всех распознанных голосовых.

Пример:

```json
[
  {
    "index": 1,
    "type": "voice",
    "text": "Хочу внедрить AI в продажи...",
    "source_url": "https://...",
    "duration_sec": 42,
    "file_size": 123456,
    "file_id": "abc",
    "created_at": "2026-06-03T...",
    "transcribed_at": "2026-06-03T..."
  },
  {
    "index": 2,
    "type": "voice",
    "text": "Ещё важно, что у нас есть CRM...",
    "source_url": "https://...",
    "duration_sec": 31,
    "file_size": 100000,
    "file_id": "def",
    "created_at": "2026-06-03T...",
    "transcribed_at": "2026-06-03T..."
  }
]
```

В исходнике каждый распознанный элемент добавляется как item с индексом, типом файла, текстом, ссылкой на источник, длительностью, размером, file_id и timestamp-полями. 

### fullTextAttr

Обычный текстовый атрибут, где все голосовые объединены в один текст.

Формат текущей сборки:

```text
[Голосовое 1]
Текст первого голосового

[Голосовое 2]
Текст второго голосового
```

Компонент строит `fullTextAttr` из массива `items`, соединяя распознанные тексты через пустую строку. 

### sourceUrlsAttr

JSON-массив ссылок на исходные голосовые файлы.

### countAttr

Количество принятых голосовых.

### statusAttr

Текущий статус сбора.

Возможные статусы:

```text
active
collecting
completed
cancelled
```

---

## Основные параметры collect()

### code

Уникальный код сессии сбора.

Пример:

```javascript
code: "demand_voice_diagnostic_intake"
```

Для повторяемых раундов удобно использовать динамический код:

```javascript
const roundCode = `demand_intake_round${currentRound}`;
```

### lead

Обязательный объект текущего лида.

```javascript
lead
```

Если `lead` не передан, компонент выбросит ошибку.

### processorScript

Код системного processor script.

```javascript
processorScript: "MultiVoiceInput_Processor"
```

### successScript

Сценарий, куда перейти после успешного завершения сбора.

```javascript
successScript: "demand_intake_analyze_round"
```

### cancelScript

Сценарий, куда перейти при отмене.

```javascript
cancelScript: "demand_intake_cancelled"
```

### errorScript

Сценарий для ошибки. В текущей конфигурации параметр есть в API и используется как часть маршрутов, но конкретную обработку ошибок нужно проверять по актуальной версии компонента.

### collectionAttr

JSON-атрибут для массива всех голосовых.

```javascript
collectionAttr: "demand_intake_round1_items_json"
```

### fullTextAttr

Текстовый атрибут с объединённой расшифровкой.

```javascript
fullTextAttr: "demand_intake_round1_full_text"
```

### sourceUrlsAttr

JSON-атрибут со ссылками на исходные файлы.

```javascript
sourceUrlsAttr: "demand_intake_round1_source_urls_json"
```

### countAttr

Атрибут с количеством голосовых.

```javascript
countAttr: "demand_intake_round1_count"
```

### statusAttr

Атрибут со статусом.

```javascript
statusAttr: "demand_intake_round1_status"
```

### extraAttrs

Дополнительные атрибуты, которые компонент устанавливает в lead.

Пример:

```javascript
extraAttrs: {
  active_agent: "metabot_intake",
  voice_context: "demand_intake_loop",
  input_mode: "multi_voice_intake_round"
}
```

В reference project через `extraAttrs` также сохраняются контекст раунда и entry-данные вроде route, funnel_id и campaign. 

---

## Лимиты

### minItems

Минимальное количество голосовых, которое нужно принять до завершения.

```javascript
minItems: 1
```

Если пользователь напишет “готово” до первого голосового, компонент покажет `messages.empty`.

### maxItems

Максимальное количество голосовых.

```javascript
maxItems: 5
```

Если пользователь достиг лимита, компонент показывает `messages.tooMany` и завершает сбор автоматически. 

### maxTotalDurationSec

Максимальная суммарная длительность всех голосовых.

```javascript
maxTotalDurationSec: 900
```

Это полезно, чтобы пользователь не отправил слишком много материала в один раунд.

### constraints.minDurationSec

Минимальная длительность одного файла.

```javascript
minDurationSec: 5
```

### constraints.maxDurationSec

Максимальная длительность одного файла.

```javascript
maxDurationSec: 600
```

### constraints.maxFileSizeBytes

Максимальный размер файла.

```javascript
maxFileSizeBytes: 20 * 1024 * 1024
```

Компонент проверяет размер, минимальную и максимальную длительность файла и возвращает пользователю понятное предупреждение, если файл не проходит ограничения. 

---

## Типы файлов

По умолчанию:

```javascript
constraints: {
  allow: {
    voice: true,
    video_note: true,
    audio: false
  }
}
```

Это значит:

```text
voice — принимаем;
video_note — принимаем;
audio — не принимаем.
```

Если нужно принимать обычные аудиофайлы, включите:

```javascript
audio: true
```

Но для Telegram voice-first сценариев обычно лучше начинать с `voice` и `video_note`.

---

## STT-настройки

Пример:

```javascript
stt: {
  provider: "openai",
  tokenKey: "OPENAI_API_KEY",
  asyncResponse: true,
  options: {
    model: "whisper-1",
    language: "ru"
  }
}
```

Поля:

```text
provider — STT-провайдер;
tokenKey — имя bot attr, где лежит ключ провайдера;
asyncResponse — использовать async callback;
options.model — модель распознавания;
options.language — язык.
```

Если `stt.tokenKey` не указан, компонент выбросит ошибку конфигурации. 

---

## UX-сообщения

Блок `messages` отвечает за пользовательские тексты.

Основные поля:

```javascript
messages: {
  wait: "🎙 Пришлите голосовое сообщение. Можно несколько подряд.",
  accepted: "✅ Принял голосовое. Расшифровываю…",
  afterItem: "Принял. Можете прислать ещё одно голосовое или написать «готово».",
  wrong: "Нужна голосовуха. Если закончили — напишите «готово». Если хотите отменить — напишите «стоп».",
  empty: "Пока нет ни одного голосового. Пришлите хотя бы одно сообщение.",
  canceled: "Ок, остановились.",
  finished: "Спасибо. Собрал контекст. Сейчас разложу задачу по карте.",
  tooMany: "Я уже собрал достаточно материала. Сейчас соберу картину.",
  tooLongTotal: "Материала уже достаточно по длительности. Сейчас соберу картину.",
  stillProcessing: "⏳ Ещё обрабатываю предыдущее голосовое…",
  transcriptionEmpty: "⚠️ Не удалось получить текст из голосового. Попробуйте записать ещё раз.",
  help: "..."
}
```

Сценарист может переопределить только нужные сообщения. Остальные возьмутся из defaults.

---

## Управляющие фразы

### finishPhrases

Фразы завершения сбора:

```javascript
finishPhrases: [
  "готово",
  "всё",
  "все",
  "закончил",
  "закончила",
  "/done"
]
```

Когда пользователь пишет такую фразу, компонент проверяет, есть ли минимум `minItems`, завершает сбор, ставит статус `completed` и запускает `successScript`.

### stopPhrases

Фразы отмены:

```javascript
stopPhrases: [
  "стоп",
  "stop",
  "отмена",
  "cancel",
  "я передумал",
  "/cancel"
]
```

При такой фразе компонент ставит статус `cancelled`, очищает активную конфигурацию и запускает `cancelScript`.

### addMorePhrases

Фразы “добавить ещё”:

```javascript
addMorePhrases: [
  "добавить ещё",
  "добавить еще",
  "еще",
  "ещё"
]
```

Компонент просто продолжает ждать голосовое.

### helpPhrases

Фразы помощи:

```javascript
helpPhrases: [
  "как записать голосовое",
  "помощь",
  "help",
  "/help"
]
```

Компонент отправляет `messages.help`.

В исходнике фразы нормализуются: текст приводится к нижнему регистру, лишние символы убираются, поддерживается точное совпадение и совпадение по началу фразы, чтобы варианты вроде “готово, собрать картину” тоже могли сработать. 

---

## Пример для AI Intake-раунда

```javascript
const MultiVoiceInput = require("Common.Voice.MultiVoiceInput");

const currentRound = parseInt(lead.getAttr("demand_intake_round") || "1");
const roundCode = `demand_intake_round${currentRound}`;

return MultiVoiceInput.collect({
  code: roundCode,

  lead,

  successScript: "demand_intake_analyze_round",
  cancelScript: "demand_intake_cancelled",
  errorScript: "demand_intake_voice_error",

  collectionAttr: `demand_intake_round${currentRound}_items_json`,
  fullTextAttr: `demand_intake_round${currentRound}_full_text`,
  sourceUrlsAttr: `demand_intake_round${currentRound}_source_urls_json`,
  countAttr: `demand_intake_round${currentRound}_count`,
  statusAttr: `demand_intake_round${currentRound}_status`,

  minItems: 1,
  maxItems: 5,
  maxTotalDurationSec: 900,

  extraAttrs: {
    active_agent: "metabot_intake",
    voice_context: "demand_intake_loop",
    input_mode: "multi_voice_intake_round",
    demand_intake_round: currentRound,
    demand_intake_round_code: roundCode
  },

  processorScript: "MultiVoiceInput_Processor",

  stt: {
    provider: "openai",
    options: {
      model: "whisper-1",
      language: "ru"
    },
    asyncResponse: true,
    tokenKey: "OPENAI_API_KEY"
  },

  constraints: {
    allow: {
      voice: true,
      video_note: true,
      audio: false
    },
    minDurationSec: 5,
    maxDurationSec: 600,
    maxFileSizeBytes: 20 * 1024 * 1024
  },

  messages: {
    wait: "🎙 Ответьте голосом. Можно несколькими сообщениями подряд.",
    accepted: "✅ Принял голосовое. Расшифровываю…",
    afterItem: "Принял. Можете записать ещё одно голосовое или нажать/написать «готово».",
    wrong: "Нужна голосовуха. Если хотите остановиться — нажмите/напишите «готово».",
    empty: "Пока нет ни одного голосового. Пришлите хотя бы одно сообщение.",
    canceled: "Ок, остановились.",
    finished: "Спасибо. Собрал контекст.",
    tooMany: "Я уже собрал достаточно материала. Сейчас обновлю картину.",
    stillProcessing: "⏳ Ещё обрабатываю предыдущее голосовое…"
  },

  finishPhrases: [
    "готово",
    "всё",
    "все",
    "закончил",
    "закончила",
    "дальше",
    "давай дальше",
    "/done"
  ]
});
```

В AI Intake reference project этот подход используется для каждого раунда: номер раунда читается из `demand_intake_round`, а результаты пишутся в `demand_intake_round{N}_items_json`, `demand_intake_round{N}_full_text`, `demand_intake_round{N}_source_urls_json`, `demand_intake_round{N}_count` и `demand_intake_round{N}_status`. 

---

## Что делать после MultiVoiceInput

После успешного завершения сбора обычно запускается сценарий анализа.

Например:

```text
demand_intake_analyze_round
```

Он берёт:

```text
demand_intake_round{N}_full_text
```

и отправляет этот текст в `LLMQuery`.

То есть `MultiVoiceInput` сам не анализирует смысл. Он только собирает и распознаёт голосовые. Анализ, JSON-сигнатура, follow-up, routing и CRM — это следующие слои сценария.

Правильное разделение:

```text
MultiVoiceInput — собрать голосовые и full_text.
LLMQuery — понять смысл и вернуть JSON.
After-analysis — показать пользователю локализованный результат.
Routing — выбрать следующий шаг.
CRM / менеджер — получить внутренний бриф.
```

---

## Как отлаживать

Отлаживать нужно по слоям.

### 1. Пользователь попал в collect script?

Проверить, что запустился сценарий, где вызывается:

```javascript
MultiVoiceInput.collect({...})
```

### 2. Активная конфигурация создана?

Проверить lead attr:

```text
__active_multi_voice_input
```

Если его нет, сбор не запущен или уже очищен.

### 3. Processor script существует?

Проверить сценарий:

```text
MultiVoiceInput_Processor
```

В нём должны быть две команды: `onCallback` и `onSTT`.

### 4. Голосовое принято?

Проверить:

```text
collectionAttr
countAttr
statusAttr
```

Если `countAttr = 0`, голосовое не добавилось в коллекцию.

### 5. STT вернул текст?

Проверить:

```text
fullTextAttr
collectionAttr
```

Если в `collectionAttr` есть item без текста или `fullTextAttr` пустой, проблема на STT-слое.

### 6. Сохранились source URLs?

Проверить:

```text
sourceUrlsAttr
```

Если пусто, возможно, проблема с получением ссылки на файл.

### 7. Завершение сработало?

Пользователь пишет:

```text
готово
```

Проверить:

```text
statusAttr = completed
```

и что запустился `successScript`.

### 8. Если пользователь пишет “готово”, но сбор не завершается

Проверить:

```text
finishPhrases;
minItems;
countAttr;
__active_multi_voice_input;
processorScript;
onCallback.
```

Если `minItems = 1`, а голосовых ещё нет, компонент должен показать `messages.empty`.

### 9. Если бот пишет “нужна голосовуха”

Проверить:

```text
тип входящего сообщения;
constraints.allow.voice;
constraints.allow.video_note;
constraints.allow.audio;
настройки канала;
payload мессенджера.
```

### 10. Если STT не стартует

Проверить:

```text
stt.provider;
stt.tokenKey;
bot attr OPENAI_API_KEY;
Run asynchronous API-request;
METABOT_API_TOKEN;
METABOT_SERVER_DOMAIN.
```

---

## Типовые ошибки

### Ошибка 1. Создали collect script, но забыли processor

Без `MultiVoiceInput_Processor` компонент не сможет принять голос и получить STT callback.

### Ошибка 2. Настроили processor как обычный JavaScript

Первая команда должна быть `Run JavaScript Callback`, вторая — `Run asynchronous API-request`.

### Ошибка 3. Не задали collectionAttr или fullTextAttr

Компонент требует эти поля, потому что ему нужно сохранить коллекцию и общий текст.

### Ошибка 4. Пользователь пишет “готово”, но ещё не отправил голосовое

При `minItems: 1` компонент не завершит сбор и покажет `messages.empty`.

### Ошибка 5. Не локализовали UX под конкретный сценарий

Default-сообщения подходят для тестов, но в production лучше писать контекстно:

```text
“Расскажите задачу голосом. Можно несколькими сообщениями подряд.”
```

### Ошибка 6. Смешали MultiVoiceInput и LLM-анализ

`MultiVoiceInput` не должен решать бизнес-задачу. Его задача — собрать голосовые и подготовить текст.

### Ошибка 7. Не проверили свежую версию компонента

Приложенный код — срез. Перед внедрением сверяйте актуальную версию плагина и параметры вызова.

---

## Короткий production checklist

```text
1. Канал принимает voice / video_note.
2. Создан MultiVoiceInput_Processor.
3. В processor есть onCallback.
4. В processor есть onSTT.
5. Настроены METABOT_API_TOKEN и METABOT_SERVER_DOMAIN.
6. Настроен OPENAI_API_KEY или другой tokenKey.
7. В collect() указан lead.
8. Указан successScript.
9. Указан cancelScript.
10. Указан collectionAttr.
11. Указан fullTextAttr.
12. Указаны sourceUrlsAttr / countAttr / statusAttr, если нужны для отладки.
13. minItems >= 1.
14. maxItems >= minItems.
15. Настроены finishPhrases.
16. Настроены stopPhrases.
17. Проверено, что fullTextAttr собирается из нескольких голосовых.
18. Проверено, что “готово” переводит в successScript.
19. Проверено, что после successScript данные уходят в LLMQuery / следующий сценарий.
```

---

## Главный вывод

`MultiVoiceInput` нужен, когда одного голосового недостаточно.

Он позволяет превратить живой человеческий рассказ, разбитый на несколько сообщений, в один структурированный вход для следующего сценарного слоя.

Формула:

```text
несколько голосовых
→ STT каждого сообщения
→ collectionAttr
→ fullTextAttr
→ successScript
→ LLMQuery / routing / CRM
```

Базовую философию голосового интерфейса, настройки канала и общую механику `VoiceInput` смотрите в основной документации:

```text
https://docs.metabot24.ru/books/06-ai-komponenty-metabot-agent-stack/page/voice-input-golosovoi-interfeis-dlya-ai-voronok
```

`VoiceInput` даёт один голосовой ответ.
`MultiVoiceInput` даёт голосовую сессию.