Голосовые NPC в Unity с памятью: полный гайд 2026 | AiManual
AiManual Logo Ai / Manual.
16 Мар 2026 Гайд

Создание голосовых NPC с памятью в Unity: полный стек (Ollama, Whisper, edge-tts, Generative Agents)

Пошаговое руководство по созданию NPC с голосом и памятью в Unity с использованием Ollama, Whisper и edge-tts. Локальный ИИ для игр.

Почему NPC в играх до сих пор разговаривают как роботы?

Открываешь диалог с торговцем в таверне. Он говорит одну из пяти заготовленных фраз. Спросишь про вчерашний праздник - он посмотрит на тебя пустым взглядом. Он не помнит, что ты спас его дочь неделю назад. Он не злится, что ты назвал его скрягой. Он просто декорация. Надоело? Мне тоже.

Проблема в архитектуре. Традиционные диалоговые системы - это конечные автоматы или дерево решений. Каждая реплика прописана вручную. Память? Только через флаги. Голос? Заранее записанные аудиофайлы. Масштабировать такое - ад. А хочется живых персонажей, которые помнят прошлые взаимодействия, имеют характер и говорят естественным голосом. И чтобы все это работало локально, без ежемесячных счетов от OpenAI.

Весь стек в одной коробке: локально, дешево, масштабируемо

Решение - собрать собственный пайплайн из open-source инструментов. Звучит сложно? На практике это три сервиса и скрипт-прослойка в Unity. Архитектура проста:

  • Whisper (v4-large) - преобразует речь игрока в текст. Локально, на вашем GPU.
  • Ollama (0.5.0 с поддержкой Llama 3.2) - локальная LLM, которая генерирует ответы NPC. Помнит контекст, обладает характером.
  • edge-tts - синтезирует речь из текста. Без GPU, использует нейронки от Microsoft через их бесплатный API (но локально можно заменить на Pocket-TTS).
  • Unity - движок, который все это связывает, управляет диалоговым интерфейсом и состоянием игры.

Ключевое слово - локально. Никаких API-ключей, никаких лимитов на запросы, никакой отправки данных игроков в облако. Весь процесс идет на вашем ПК или сервере. Для продакшена это значит предсказуемые затраты и полный контроль.

1 Готовим движок: Ollama и свежая модель

Сначала ставим Ollama. На 16.03.2026 актуальная версия - 0.5.0. Она поддерживает новые квантованные форматы моделей (например, Q4_K_M), что экономит память без сильной потери качества.

# Установка Ollama на Linux/macOS
curl -fsSL https://ollama.ai/install.sh | sh

# Запуск сервиса
ollama serve &

# Скачиваем модель. Llama 3.2 8B - хороший баланс скорости и интеллекта для NPC.
ollama pull llama3.2:8b

Почему не Mistral или другие? Llama 3.2 лучше понимает инструкции по формированию личности персонажа и стабильнее ведет себя в длинных диалогах. Но если у вас слабая видеокарта, попробуйте Phi-4:3.8b - новая мини-модель от Microsoft, которая удивляет качеством.

💡
Ollama 0.5.0 добавила встроенную поддержку системного промпта через файл Modelfile. Это наш секрет для создания характера NPC. Создайте файл npc_merchant.Modelfile с инструкцией: "Ты - Гарольд, скупой торговец в таверне 'Гномий кубок'. Ты подозрительный и жаден. Отвечай коротко, используй сленг. Помни все прошлые сделки с игроком." Затем создайте модель: ollama create merchant -f ./npc_merchant.Modelfile. Теперь при каждом запросе характер будет вшит в контекст.

2 Уши для NPC: настраиваем Whisper для распознавания речи

Whisper от OpenAI - все еще король распознавания речи. Берем последнюю версию v4-large через Hugging Face. Но есть нюанс: оригинальный Whisper жрет много памяти. Вместо него используйте faster-whisper - та же точность, но в 4 раза быстрее и с поддержкой GPU через CTranslate2.

pip install faster-whisper
# minimal_whisper_server.py
from faster_whisper import WhisperModel
import numpy as np
from flask import Flask, request, jsonify

app = Flask(__name__)
model = WhisperModel("large-v4", device="cuda", compute_type="float16")

@app.route('/transcribe', methods=['POST'])
def transcribe():
    audio_data = request.files['audio'].read()
    # Конвертируем raw bytes в numpy array (предполагаем 16kHz mono)
    audio_np = np.frombuffer(audio_data, dtype=np.int16).astype(np.float32) / 32768.0
    segments, info = model.transcribe(audio_np, beam_size=5, language='ru')
    text = ' '.join([seg.text for seg in segments])
    return jsonify({'text': text})

if __name__ == '__main__':
    app.run(port=5001)

Запустите этот сервер. Он будет слушать порт 5001 и ждать аудио в формате raw PCM. Из Unity вы будете отправлять запись микрофона. Задержка? На RTX 4070 - около 0.8 секунды на 5 секунд речи. Приемлемо для диалога.

3 Голос из ничего: синтез речи через edge-tts

Тут вариантов много. edge-tts - простой, с кучей голосов (даже русские), но требует интернета. Если нужна полная локальность, смотрите в сторону XTTS-v2 или Piper. Но для прототипа edge-tts идеален.

# tts_server.py
import asyncio
import edge_tts
from flask import Flask, request, jsonify
import os

app = Flask(__name__)
VOICE = "ru-RU-SvetlanaNeural"  # Попробуйте также ru-RU-DmitryNeural

async def generate_speech(text, output_file):
    communicate = edge_tts.Communicate(text, VOICE)
    await communicate.save(output_file)

@app.route('/synthesize', methods=['POST'])
def synthesize():
    data = request.json
    text = data.get('text', '')
    if not text:
        return jsonify({'error': 'No text'}), 400
    output_path = f"temp_{hash(text)}.mp3"
    asyncio.run(generate_speech(text, output_path))
    # Читаем файл и отправляем байты
    with open(output_path, 'rb') as f:
        audio_bytes = f.read()
    os.remove(output_path)
    return audio_bytes, 200, {'Content-Type': 'audio/mpeg'}

if __name__ == '__main__':
    app.run(port=5002)

Сервер возвращает готовый MP3. В Unity вы его проигрываете через AudioSource. Задержка синтеза - около 1-2 секунд. Можно кэшировать часто используемые фразы.

4 Мозги и память: архитектура Generative Agents в Unity

Самое интересное. Нужно, чтобы NPC не просто отвечал на последнюю реплику, а помнил весь диалог и ключевые события. Берем идеи из Stanford Generative Agents. Упрощенная реализация:

  • Память как векторная база: Каждое высказывание или событие ("игрок купил меч", "игрок оскорбил торговца") преобразуем в эмбеддинг через какую-нибудь легкую модель (например, all-MiniLM-L6-v2) и кладем в векторную БД типа Chroma.
  • Поиск релевантных воспоминаний: Перед генерацией ответа, преобразуем текущий запрос игрока в эмбеддинг, ищем в БД топ-5 самых похожих прошлых событий.
  • Системный промпт с контекстом: В промпт для Ollama включаем найденные воспоминания и инструкцию характера. Это создает иллюзию долгосрочной памяти.
# agent_brain.py (упрощенно)
import ollama
import chromadb
from sentence_transformers import SentenceTransformer

embedder = SentenceTransformer('all-MiniLM-L6-v2')
chroma_client = chromadb.PersistentClient(path="./npc_memory")
collection = chroma_client.get_or_create_collection(name="harold_memories")

class NPCBrain:
    def __init__(self, npc_name):
        self.npc_name = npc_name
        self.conversation_history = []  # Последние 10 реплик
    
    def add_memory(self, description):
        # Сохраняем событие в векторную БД
        embedding = embedder.encode(description).tolist()
        collection.add(
            documents=[description],
            embeddings=[embedding],
            ids=[str(hash(description))]
        )
    
    def recall(self, query, n=5):
        # Вспоминаем похожие события
        query_embedding = embedder.encode(query).tolist()
        results = collection.query(
            query_embeddings=[query_embedding],
            n_results=n
        )
        return results['documents'][0] if results['documents'] else []
    
    def generate_response(self, player_input):
        # 1. Вспоминаем
        memories = self.recall(player_input)
        # 2. Формируем промпт
        prompt = f"""Ты - {self.npc_name}. Твои воспоминания: {', '.join(memories)}.
        Текущий разговор: {' '.join(self.conversation_history[-5:])}
        Игрок: {player_input}
        {self.npc_name}: """
        # 3. Запрос к Ollama
        response = ollama.chat(model='merchant', messages=[{'role': 'user', 'content': prompt}])
        npc_text = response['message']['content']
        # 4. Сохраняем реплику в историю и память
        self.conversation_history.append(f"Игрок: {player_input}")
        self.conversation_history.append(f"{self.npc_name}: {npc_text}")
        self.add_memory(f"Диалог: {player_input[:50]}...")
        return npc_text

Этот скрипт - мозг NPC. Он работает как отдельный сервис на порту 5003. Unity отправляет сюда текст от игрока, получает ответ и аудио от TTS сервера.

5 Связываем все в Unity: C# скрипт-оркестратор

В Unity создаем пустой GameObject "NPCDialogueManager". Вешаем на него скрипт.

// NPCDialogueController.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Networking;
using System;

public class NPCDialogueController : MonoBehaviour
{
    public AudioSource audioSource;
    public Button recordButton;
    public Text dialogueText;
    
    private string whisperUrl = "http://localhost:5001/transcribe";
    private string brainUrl = "http://localhost:5003/generate";
    private string ttsUrl = "http://localhost:5002/synthesize";
    
    private AudioClip recordingClip;
    private bool isRecording = false;
    
    void Start()
    {
        recordButton.onClick.AddListener(ToggleRecording);
    }
    
    void ToggleRecording()
    {
        if (!isRecording)
        {
            StartRecording();
        }
        else
        {
            StopRecordingAndProcess();
        }
    }
    
    void StartRecording()
    {
        recordingClip = Microphone.Start(null, false, 10, 16000);
        isRecording = true;
        recordButton.GetComponentInChildren().text = "Стоп...";
    }
    
    async void StopRecordingAndProcess()
    {
        Microphone.End(null);
        isRecording = false;
        recordButton.GetComponentInChildren().text = "Говорить";
        
        // 1. Отправляем аудио в Whisper
        byte[] audioBytes = AudioClipToRawBytes(recordingClip);
        string playerText = await TranscribeAudio(audioBytes);
        dialogueText.text = "Игрок: " + playerText;
        
        // 2. Отправляем текст в мозг NPC
        string npcText = await GetNPCResponse(playerText);
        dialogueText.text += "\nNPC: " + npcText;
        
        // 3. Синтезируем и проигрываем речь
        AudioClip npcSpeech = await SynthesizeSpeech(npcText);
        audioSource.clip = npcSpeech;
        audioSource.Play();
    }
    
    byte[] AudioClipToRawBytes(AudioClip clip)
    {
        float[] samples = new float[clip.samples * clip.channels];
        clip.GetData(samples, 0);
        byte[] bytes = new byte[samples.Length * 2];
        for (int i = 0; i < samples.Length; i++)
        {
            short intSample = (short)(samples[i] * 32767);
            BitConverter.GetBytes(intSample).CopyTo(bytes, i * 2);
        }
        return bytes;
    }
    
    // Далее методы TranscribeAudio, GetNPCResponse, SynthesizeSpeech с использованием UnityWebRequest
    // Для асинхронности используйте UniTask или корутины
}

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

Где собака зарыта: нюансы, которые сведут с ума

Проблема Причина Решение
Задержка 5-10 секунд между репликами Whisper медленный, LLM думает, TTS генерирует. Все последовательно. Сделать пайплайн параллельным: пока Whisper работает, можно показывать анимацию "NPC думает". Кэшировать частые ответы.
NPC забывает разговор после перезагрузки Векторная БД в памяти, Chroma на диске но не интегрирована с игровыми событиями. Привязывать сохранения памяти к системе сохранений игры. Сериализовать ключевые воспоминания в файл сцены.
Голос звучит как робот, нет эмоций edge-tts использует стандартные нейронные голоса без настройки. Перейти на локальный XTTS-v2 и добавлять в промпт эмоциональные метки: [грустно], [зло] и т.д. Или использовать AnyTTS для гибкости.

Вопросы, которые вы зададите через час отладки

Сколько VRAM нужно для всего этого? Минимум 8 GB. Разброс: Whisper large-v4 - 4 GB, Llama 3.2 8B в 4-битном квантовании - 5 GB, модель для эмбеддингов - 1 GB. Итого 10 GB. Если видеокарты слабее, берите smaller модели: Whisper medium, Llama 3.2 3B, эмбеддер tiny.

Как запустить это на продакшене для MMO? Никак. Эта архитектура для синглплеера или кооператива на 4 человека. Для MMO нужно выносить сервисы Ollama и Whisper на отдельный сервер с GPU, масштабировать через очередь запросов и балансировщик. И тогда считайте стоимость инстансов с A100. Но для инди-игры - то что надо.

NPC начал нести чушь или говорит слишком долго? Ограничивайте ответ 3-4 предложениями в системном промпте. Добавьте инструкцию: "Отвечай кратко, 1-2 предложениями". Если NPC уходит в философские рассуждения, обрежьте ответ на стороне Unity после 100 символов.

Что дальше? Эволюция агента

Базовая система работает. Теперь можно улучшать:

  • Визуальная связь: Подключите PersonaPlex от NVIDIA для синхронизации мимики рта с речью.
  • Мультиагентность: Несколько NPC могут общаться между собой. Нужен координатор, как в этой архитектуре.
  • Планирование: NPC с целями ("заработать 100 золотых") и автономными действиями. Сложно, но возможно.

Главное - начать. Соберите минимальный работающий прототип за выходные. Пусть ваш торговец сначала будет просто отвечать на вопросы о погоде. Но он уже будет делать это своим голосом. И, возможно, запомнит, что вы спросили об этом три раза подряд. А в следующий раз скажет: "Опять про погоду? Купи лучше зелье, простудишься." Вот это уже характер.

Подписаться на канал