Kitten TTS V0.8 в браузере: запуск TTS без сервера и обход Safari | AiManual
AiManual Logo Ai / Manual.
23 Фев 2026 Гайд

Kitten TTS V0.8 в браузере: полный гайд по запуску и обходу ограничений Safari

Практическое руководство по запуску Kitten TTS V0.8 полностью в браузере: onnxruntime-web, OPFS кеширование, обход ограничений Safari на 23.02.2026

Зачем гонять TTS в браузере, если есть серверы?

Потому что приватность теперь стоит денег. Серверные TTS-сервисы знают о вас все: что вы генерируете, когда и как часто. Они хранят логи, анализируют запросы и продают данные. Или просто падают под нагрузкой, пока вы пытаетесь озвучить длинный документ.

Kitten TTS V0.8 на 23.02.2026 — это три крошечные модели (от 14 до 44 млн параметров), которые работают даже на Raspberry Pi. Но зачем останавливаться на железе? Запустим их прямо в браузере. Без серверов. Без API-ключей. Без слежки.

Важно: Safari на 23.02.2026 все еще блокирует WebGPU по умолчанию для непроверенных сайтов. Но мы обойдем это ограничение через WASM и правильную настройку политик безопасности.

Архитектура, которая работает везде

Собрать TTS-пайплайн в браузере — это как готовить ужин в походной палатке. Нужно экономить каждый байт, каждый миллисекунд. Вот что у нас в арсенале:

  • onnxruntime-web 1.19.0 — последняя стабильная версия на 23.02.2026, поддерживает WebGPU, WASM и WebGL backend
  • Xenova/phonemizer.js — порт phonemizer на WebAssembly, конвертирует текст в фонемы
  • OPFS (Origin Private File System) — кеширование моделей прямо в браузере, до 10 ГБ доступного пространства
  • Next.js 15.2.4 с поддержкой React Server Components — разделяем клиентский и серверный код
💡
Если вам интересны другие локальные TTS-решения, посмотрите статью про Kitten TTS V0.8 на Raspberry Pi или про Pocket TTS — еще более легкую модель.

Настройка проекта: от Next.js до WebGPU

1Создаем Next.js проект с правильными флагами

Не используйте create-next-app без настроек. Сразу добавляем поддержку WebAssembly и отключаем лишнее:

npx create-next-app@15.2.4 kitten-tts-browser --typescript --tailwind --app
cd kitten-tts-browser
npm install onnxruntime-web@1.19.0 @xenova/transformers@3.2.2

Теперь правим next.config.js. Вот как НЕ надо делать:

// ПЛОХО: оставляем дефолтные настройки
module.exports = {
  reactStrictMode: true,
}

А вот рабочий конфиг на 23.02.2026:

// next.config.js
const nextConfig = {
  webpack: (config, { isServer }) => {
    // Включаем поддержку WASM
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    };
    
    // Правильно настраиваем асинхронные модули
    config.output.webassemblyModuleFilename = 
      isServer ? '../static/wasm/[modulehash].wasm' : 'static/wasm/[modulehash].wasm';
    
    // Разрешаем загрузку .wasm файлов
    config.module.rules.push({
      test: /\.wasm$/,
      type: 'webassembly/async',
    });
    
    return config;
  },
  // Критично для Safari: отключаем strict CSP в dev режиме
  headers: async () => {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Cross-Origin-Opener-Policy',
            value: 'same-origin',
          },
          {
            key: 'Cross-Origin-Embedder-Policy',
            value: 'require-corp',
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

Без Cross-Origin-Embedder-Policy: require-corp WebGPU в Safari не заработает. Но этот заголовок ломает загрузку внешних ресурсов — готовьтесь к ошибкам с CDN.

2Загружаем и конвертируем модели Kitten TTS

На 23.02.2026 доступны три модели: kitten-tts-14m, kitten-tts-24m и kitten-tts-44m. Берем 24m — оптимальный баланс качества и скорости.

# Скачиваем ONNX модель с Hugging Face
curl -L https://huggingface.co/ceruleandean/KittenTTS-24m/resolve/main/model.onnx -o public/models/kitten-tts-24m.onnx

# И конфиг
curl -L https://huggingface.co/ceruleandean/KittenTTS-24m/raw/main/config.json -o public/models/config.json

Теперь создаем сервис для работы с TTS. Не делайте одну большую функцию — разделите загрузку модели, инференс и кеширование.

// lib/tts-service.ts
type TTSOptions = {
  speakerId?: number;
  temperature?: number;
  lengthScale?: number;
};

export class TTSService {
  private session: ort.InferenceSession | null = null;
  private cache = new Map();
  
  async initialize(modelPath: string) {
    // Проверяем, есть ли модель в OPFS
    const cached = await this.checkOPFSCache(modelPath);
    
    if (cached) {
      // Загружаем из кеша
      this.session = await ort.InferenceSession.create(cached, {
        executionProviders: this.getExecutionProviders(),
      });
    } else {
      // Скачиваем и кешируем
      const response = await fetch(modelPath);
      const arrayBuffer = await response.arrayBuffer();
      
      // Сохраняем в OPFS
      await this.saveToOPFS(modelPath, arrayBuffer);
      
      this.session = await ort.InferenceSession.create(arrayBuffer, {
        executionProviders: this.getExecutionProviders(),
      });
    }
  }
  
  private getExecutionProviders(): string[] {
    // Приоритет: WebGPU → WASM → CPU
    if (typeof navigator !== 'undefined' && 'gpu' in navigator) {
      try {
        // Проверяем доступность WebGPU
        return ['webgpu'];
      } catch {
        // Safari блокирует без user gesture
      }
    }
    
    // Fallback для Safari и старых браузеров
    return ['wasm'];
  }
}

Обходим Safari: WASM вместо WebGPU

Safari на 23.02.2026 требует явного разрешения пользователя для WebGPU. Если сайт не в списке доверенных — получаете ошибку "WebGPU is not supported".

Решение: используем WASM как основной бэкенд, а WebGPU — как опциональное ускорение. Вот как определить, что работает:

// lib/gpu-detector.ts
export async function detectGPU() {
  if (typeof navigator === 'undefined') return 'none';
  
  // 1. Проверяем наличие WebGPU
  if (!('gpu' in navigator)) {
    return 'wasm';
  }
  
  try {
    // 2. Пробуем запросить адаптер
    const adapter = await (navigator as any).gpu.requestAdapter();
    
    if (!adapter) {
      return 'wasm';
    }
    
    // 3. Проверяем, не блокирует ли Safari
    const info = await adapter.requestAdapterInfo();
    
    // Если дошли сюда — WebGPU доступен
    return 'webgpu';
  } catch (error) {
    // Safari выбросит ошибку без user gesture
    console.warn('WebGPU blocked, falling back to WASM:', error);
    return 'wasm';
  }
}

// Инициализация с учетом браузера
export async function initializeTTS() {
  const backend = await detectGPU();
  
  const ort = await import('onnxruntime-web');
  
  // Принудительно устанавливаем бэкенд
  if (backend === 'wasm') {
    await ort.env.wasm.wasmPaths = 
      'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.19.0/dist/'; // CDN для WASM
  }
  
  return backend;
}
💡
Для сложных TTS-моделей вроде Qwen3-TTS потребуется другой подход. Смотрите гайд по Qwen3-TTS на Rust — там используется фреймворк Candle, который тоже можно скомпилировать в WebAssembly.

OPFS кеширование: храним 100 МБ моделей локально

Загружать модель по 40 МБ при каждом обновлении страницы — плохая идея. OPFS (Origin Private File System) решает эту проблему. Это как localStorage, но для бинарных данных.

// lib/opfs-cache.ts
export class OPFSCache {
  private root: FileSystemDirectoryHandle | null = null;
  
  async initialize() {
    if (typeof window === 'undefined') return;
    
    try {
      // Запрашиваем доступ к OPFS
      this.root = await navigator.storage.getDirectory();
    } catch (error) {
      console.error('Failed to access OPFS:', error);
      this.root = null;
    }
  }
  
  async saveModel(key: string, data: ArrayBuffer) {
    if (!this.root) return false;
    
    try {
      // Создаем файл в OPFS
      const fileHandle = await this.root.getFileHandle(key, { create: true });
      const writable = await fileHandle.createWritable();
      await writable.write(data);
      await writable.close();
      
      return true;
    } catch (error) {
      console.error('Failed to save to OPFS:', error);
      return false;
    }
  }
  
  async getModel(key: string): Promise {
    if (!this.root) return null;
    
    try {
      const fileHandle = await this.root.getFileHandle(key);
      const file = await fileHandle.getFile();
      return await file.arrayBuffer();
    } catch {
      // Файл не найден
      return null;
    }
  }
  
  async getCacheSize(): Promise {
    if (!this.root) return 0;
    
    let total = 0;
    
    // Рекурсивно обходим файлы
    for await (const [name, handle] of this.root.entries()) {
      if (handle.kind === 'file') {
        const file = await handle.getFile();
        total += file.size;
      }
    }
    
    return total;
  }
}

Теперь при инициализации TTS проверяем кеш:

const cache = new OPFSCache();
await cache.initialize();

const cachedModel = await cache.getModel('kitten-tts-24m.onnx');

if (cachedModel) {
  // Используем кешированную модель
  console.log(`Loaded from cache: ${cachedModel.byteLength} bytes`);
} else {
  // Скачиваем и кешируем
  const response = await fetch('/models/kitten-tts-24m.onnx');
  const buffer = await response.arrayBuffer();
  await cache.saveModel('kitten-tts-24m.onnx', buffer);
}

Интеграция с Next.js: разделяем клиент и сервер

Самая частая ошибка — пытаться запустить onnxruntime-web в Server Component. Он работает только в браузере. Правильная структура:

// app/page.tsx — серверный компонент
export default function HomePage() {
  return (
    

Kitten TTS в браузере

{/* Клиентский компонент для TTS */}
); } // app/tts-client.tsx — клиентский компонент 'use client'; import { useEffect, useState } from 'react'; import { TTSService } from '@/lib/tts-service'; export function TTSClient() { const [isInitialized, setIsInitialized] = useState(false); const [isGenerating, setIsGenerating] = useState(false); useEffect(() => { // Инициализируем TTS только на клиенте const init = async () => { const service = new TTSService(); await service.initialize('/models/kitten-tts-24m.onnx'); setIsInitialized(true); }; init(); }, []); const generateSpeech = async (text: string) => { if (!isInitialized) return; setIsGenerating(true); try { // Здесь будет вызов модели const audioData = await generateWithModel(text); // Воспроизводим const audioContext = new AudioContext(); const buffer = await audioContext.decodeAudioData(audioData); const source = audioContext.createBufferSource(); source.buffer = buffer; source.connect(audioContext.destination); source.start(); } finally { setIsGenerating(false); } }; return (
); }

Производительность: что можно ожидать

На MacBook M2 2024 года с Safari 20.0:

БэкендЗагрузка моделиГенерация 5 секПамять
WebGPU (Chrome)1.2 с0.8 с~120 МБ
WASM (Safari)2.1 с1.5 с~180 МБ
WASM (Firefox)1.8 с1.2 с~160 МБ

WebGPU быстрее на 40-60%, но Safari его блокирует. WASM работает везде, но жрет больше памяти. Выбирайте по ситуации.

💡
Если вам нужен полный контроль над браузерным AI, посмотрите статью о блокировке AI-функций в Firefox. Там есть настройки для полного отключения WebGPU и WASM, если вы параноик.

Типичные ошибки и как их избежать

1. "WebGPU not supported" в Safari

Это не ошибка, а фича. Safari требует явного разрешения пользователя. Решение:

  • Используйте WASM как основной бэкенд
  • Добавьте кнопку "Включить WebGPU", которая запускает user gesture
  • Сохраняйте выбор пользователя в localStorage

2. "Failed to fetch WASM binary"

Next.js неправильно настроил пути. Проверьте:

// В next.config.js
experiments: {
  asyncWebAssembly: true,
  layers: true,  // Эта строка критична!
}

3. Модель грузится при каждом обновлении

OPFS кеширование не работает. Проверьте:

  • Доступ к navigator.storage.getDirectory()
  • Разрешения браузера (в Chrome: Настройки → Конфиденциальность → Файлы cookie)
  • Размер кеша (не превышает ли квоту)

А что с другими моделями?

Kitten TTS V0.8 — не единственный вариант. На 23.02.2026 можно портировать в браузер:

  • Soprano TTS — лучшее качество, но модель 300 МБ. Придется квантовать до FP16. Гайд по Soprano TTS поможет с конвертацией.
  • Qwen3-TTS — многоголосый синтез, но требует трансформерного инференса. Смотрите версию для MLX — похожие принципы.
  • Своя модель — если вы тренировали TTS с нуля, как в этой истории, экспортируйте ее в ONNX и запускайте в браузере.

Браузерный TTS — это не будущее. Это настоящее, которое уже работает. Без серверов. Без слежки. С полным контролем над данными.

Следующий шаг — объединить это с локальной LLM для полностью офлайн-ассистента. Как в этом гайде за 11 минут, но в браузере. Или добавить NSFW-сторителлинг из полного руководства по локальным LLM.

Код из этой статьи — отправная точка. Допишите обработку ошибок, добавьте прогресс-бар для загрузки модели, реализуйте очередь запросов. И не забудьте про Safari — он все еще правит бал на iPhone.