Модели речь-в-речь

Soniqo поддерживает два семейства нативных MLX-моделей для диалога речь-в-речь: PersonaPlex 7B и VoiceChat 11B. Обе принимают речь и напрямую генерируют аудио модели, но используют разные дуплексные архитектуры и контракты рантайма.

Выбор модели

МодельДиалоговая схемаЛучше всего подходит для
PersonaPlex 7BОдновременный полный дуплекс на Moshi, Depformer и MimiПерекрывающихся слушания и речи, 18 выбираемых голосов
VoiceChat 11BНепрерывный покадрово-синхронный дуплекс на FastConformer, Nemotron-H, EAR-TTS и нейронном кодекеПотоковых кадров по 80 ms, текстовых/функциональных событий и голоса из checkpoint

Это диалоговые модели речь-в-речь. Hibiki тоже напрямую преобразует речь в речь, но решает задачу потокового речевого перевода и поэтому описан отдельно.

PersonaPlex 7B

Модель полнодуплексного диалога речь-в-речь на базе архитектуры Moshi (Kyutai). PersonaPlex 7B генерирует голосовые ответы непосредственно из голосового входа — без промежуточного текстового пайплайна. Модель поставляется с 18 голосовыми пресетами и доступна в 8-битной (рекомендуется) и 4-битной квантизации. По умолчанию 8-bit — она на 30% быстрее и выдаёт связные ответы, тогда как 4-bit ухудшает качество вывода.

VoiceChat 11B

Архитектурная ссылка для дуплексного speech-to-speech: SALM-Duplex: Efficient and Direct Duplex Modeling for Speech-to-Speech Language Model (Interspeech 2025), указанная в upstream-карточке VoiceChat NVIDIA.

M5 Pro, 48 ГБ, Release, live-такт 80 мс, INT5 с защищёнными головами: три контролируемых запуска дали RTF всего пайплайна 0.94, 0.92 и 0.92 при пиковом RSS около 8.70 GB. Первый произнесённый текст появился через 44.3–46.7 ms, а первое готовое к воспроизведению аудио — через 74.0–76.4 ms. Это прогретые вычислительные значения, а не выученная задержка смены реплики; оптимизированная INT8 ещё не перемерена.

VoiceChat — второй нативный MLX-рантайм речь-в-речь в speech-swift. Он объединяет каузальное восприятие FastConformer, дуплексные текстовый/функциональный каналы Nemotron-H, автономный текстово-обусловленный EAR-TTS и нейронный кодек 22,05 кГц. Сессия непрерывно принимает моно-аудио 16 кГц и каждые 80 мс возвращает текстовые события и выходной кадр на 1 764 отсчёта.

Загрузите модель через VoiceChatModel.load(from:), запустите VoiceChatSession и передавайте аудио с pushAudio. Полный bundle должен содержать encoder/, llm/ и tts/; старые bundles только для понимания отклоняются.

Текущая производительность

INT5 на протестированном M5 Pro удерживает среднюю пропускную способность ниже RTF 1. Во всех трёх запусках общий p95 на кадр также был ниже 80 ms и составил 76.2–78.6 ms; поэтому запас по срокам остаётся небольшим. Начало реплики модели и вычислительную задержку оборудования следует измерять отдельно.

Архитектура и использование PersonaPlex

PersonaPlex — это многопоточная авторегрессивная модель с тремя ключевыми компонентами:

КомпонентДетали
Temporal Transformer32 слоя, dim=4096, 32 head, SwiGLU (hidden_scale=4.125), RoPE, 8-bit-квантизация (по умолчанию)
Depformer6 слоёв, dim=1024, 16 head, MultiLinear (weights_per_step=true), dep_q=16
Mimi Codec16 codebook, частота кадров 12.5 Гц, аудио-выход 24 кГц

Модель обрабатывает 17 потоков одновременно: 1 текстовый поток + 8 пользовательских аудиопотоков + 8 аудиопотоков агента. Эта архитектура позволяет полнодуплексный диалог, где модель может одновременно слушать и говорить.

Голосовые пресеты

PersonaPlex включает 18 встроенных голосовых пресетов в натуральном и разнообразных стилях:

КатегорияПресеты
Natural FemaleNATF0, NATF1, NATF2, NATF3
Natural MaleNATM0, NATM1, NATM2, NATM3
Varied FemaleVARF0, VARF1, VARF2, VARF3, VARF4
Varied MaleVARM0, VARM1, VARM2, VARM3, VARM4

Внутренний монолог

PersonaPlex на каждом шаге генерирует два параллельных потока: 8 audio-codebook-токенов для Mimi-кодека и один текстовый токен для внутреннего монолога модели. Текстовый поток — это то, что модель «думает», пока говорит — он может немного расходиться с финальным аудио, но на практике достаточно близко повторяет голосовой ответ, чтобы использовать его как живой транскрипт.

Текстовые токены возвращаются как сырые SentencePiece piece ID. Декодируйте их через SentencePieceDecoder, который поставляется в PersonaPlex:

import PersonaPlex
import AudioCommon

let model = try await PersonaPlexModel.fromPretrained()
let decoder = model.tokenizer  // SentencePieceDecoder?

let result = model.respond(userAudio: userSamples, voice: .NATM0)
let transcript = decoder?.decode(result.textTokens) ?? ""
print(transcript)        // "Sure, I can help with that..."
playAudio(result.audio)  // 24 kHz mono Float32

В потоковом режиме respondStream выдаёт куски textTokens по мере их появления — декодируйте их инкрементально, чтобы обновлять живые субтитры, пока аудио всё ещё генерируется. Флаг CLI --transcript делает именно это за кулисами.

Почему это важно: SentencePieceDecoder построен на общем AudioCommon.SentencePieceModel-ридере protobuf, так что PersonaPlex, OmnilingualASR и любые будущие модели на SentencePiece декодируют через одну реализацию токенизатора. См. справочник SentencePieceModel.

Системные промпты

Передайте любой пользовательский системный промпт как обычную строку — внешняя токенизация не нужна:

let response = model.respond(
    userAudio: audio,
    voice: .NATM0,
    systemPrompt: "You enjoy having a good conversation."
)

Или используйте встроенный пресет:

Использование CLI

Сгенерировать голосовой ответ из аудио-входа:

# Basic speech-to-speech
.build/release/speech respond --input question.wav

# Choose a voice preset
.build/release/speech respond --input question.wav --voice NATM0

# Stream audio output during generation
.build/release/speech respond --input question.wav --stream

# Custom system prompt text
.build/release/speech respond --input question.wav --system-prompt-text "You enjoy having a good conversation."

# Use a preset system prompt
.build/release/speech respond --input question.wav --system-prompt customer-service

# Get transcript alongside audio
.build/release/speech respond --input question.wav --transcript

# JSON output with metadata
.build/release/speech respond --input question.wav --json

Опции

ОпцияОписание
--inputВходной аудиофайл (WAV, обязательно)
--voiceИмя голосового пресета (например, NATM0, VARF2)
--system-promptПресет системного промпта: assistant, focused, customer-service, teacher
--system-prompt-textПользовательский текст системного промпта (переопределяет --system-prompt)
--max-stepsМаксимум шагов генерации
--streamВыдавать аудио-чанки во время генерации
--compileИспользовать compiled-инференс MLX для более быстрой генерации
--transcriptВыводить текстовый транскрипт вместе с аудио
--jsonJSON-вывод с метаданными

Параметры сэмплирования также можно переопределить:

ОпцияПо умолчаниюОписание
--audio-temp0.8Температура сэмплирования audio-токенов
--audio-top-k250Top-k сэмплирование audio-токенов
--text-temp0.7Температура сэмплирования текстовых токенов
--text-top-k25Top-k сэмплирование текстовых токенов

Потоковый режим

Флаг --stream включает вывод аудио в реальном времени. Аудио-чанки выдаются по мере их генерации, так что воспроизведение может начаться до того, как полный ответ будет готов. Это особенно полезно для интерактивных приложений, где важна низкая задержка.

Производительность

МетрикаЗначение
Real-time factor (RTF)~1.4 (8-bit, почти реальное время)
Задержка шага~112 мс/шаг на M2 Max (8-bit)
Размер модели (8-bit)~9.1 ГБ
Пиковая RAM (8-bit)~11 ГБ
Размер модели (4-bit)~4.9 ГБ
Пиковая RAM (4-bit)~7 ГБ
Важно

PersonaPlex 7B (8-bit) требует минимум 24 ГБ RAM. Вариант 4-bit помещается на устройства с 16 ГБ, но выдаёт ухудшенный вывод. На устройствах с 8 ГБ ни один вариант не поместится. Используйте --compile для лучшей производительности на поддерживаемом оборудовании.

Варианты модели

МодельРазмерHuggingFace
PersonaPlex-7B (8-bit) рекомендуется9.1 ГБaufklarer/PersonaPlex-7B-MLX-8bit
PersonaPlex-7B (4-bit)4.9 ГБaufklarer/PersonaPlex-7B-MLX-4bit

Swift API

import PersonaPlex
import AudioCommon

let model = try await PersonaPlexModel.fromPretrained()
let response = model.respond(
    userAudio: userSamples,
    voice: .NATM0,
    systemPrompt: "You are a helpful assistant."
)
try WAVWriter.write(samples: response.audio, sampleRate: 24000, to: URL(filePath: "answer.wav"))