AI-радиостанция своими руками: Qwen 1.5B и Piper TTS | VibeCast | AiManual
AiManual Logo Ai / Manual.
01 Янв 2026 Гайд

Как создать AI-радиостанцию на базе Qwen 1.5B и Piper TTS: туториал по VibeCast

Пошаговый гайд по созданию локальной AI-радиостанции с генерацией скриптов, синтезом речи и веб-интерфейсом. Работает полностью офлайн.

Представьте: утренний кофе, а вместо скучных новостей или навязчивой рекламы — персонализированная радиостанция, которая говорит с вами на одном языке, обсуждает интересные именно вам темы и делает это с теплым, почти человеческим голосом. Это не фантастика, а реальный проект VibeCast, который мы сегодня разберем по косточкам.

Проблема современного медиапотребления в его обезличенности. Алгоритмы крупных платформ предлагают одно и то же миллионам пользователей, а если и пытаются персонализировать, то делают это поверхностно. VibeCast решает эту проблему, предлагая полностью локальное решение, где вы контролируете и контент, и голос, и саму логику вещания.

Что такое VibeCast? Это полноценная радиостанция на базе AI, которая генерирует скрипты выпусков с помощью Qwen 1.5B, озвучивает их через Piper TTS и транслирует через веб-интерфейс. Всё работает на вашем компьютере без интернета.

Архитектура решения

Прежде чем погружаться в код, давайте разберемся, как устроена система. VibeCast состоит из трех основных компонентов:

Компонент Технология Назначение
Генератор контента Ollama + Qwen 1.5B Создание радиоскриптов по темам
Синтезатор речи Piper TTS Преобразование текста в аудио
Веб-сервер и интерфейс FastAPI + React Управление и прослушивание

Ключевое преимущество этой архитектуры — полная локальность. В отличие от облачных решений, ваши данные никуда не уходят, вы не зависите от API-лимитов и можете кастомизировать систему как угодно. Если вам интересна тема локального синтеза речи, рекомендую почитать обзор инструмента with.audio для браузерного синтеза.

1 Подготовка окружения

Начнем с установки базовых компонентов. Вам понадобится Python 3.9+ и около 4 ГБ свободного места на диске для моделей.

# Создаем виртуальное окружение
python -m venv vibecast_env
source vibecast_env/bin/activate  # На Windows: vibecast_env\Scripts\activate

# Устанавливаем зависимости
pip install fastapi uvicorn ollama python-multipart pydantic
pip install piper-tts  # или используем pip install TTS для альтернативы
💡
Если у вас есть видеокарта NVIDIA, установите CUDA-версии библиотек для ускорения. Для AMD карт потребуется ROCm — подробности в гайде по сборке стека для локальных LLM.

2 Установка и настройка Ollama с Qwen 1.5B

Qwen 1.5B — идеальный выбор для нашей задачи: достаточно маленький для работы на CPU, но достаточно умный для генерации связных радиоскриптов.

# Устанавливаем Ollama (Linux/Mac)
curl -fsSL https://ollama.ai/install.sh | sh

# Скачиваем модель Qwen 1.5B
ollama pull qwen:1.5b

# Проверяем работу
ollama run qwen:1.5b "Напиши приветствие для радиостанции"

Важно: Если у вас менее 8 ГБ оперативной памяти, рассмотрите вариант с Qwen:0.5b или используйте квантованную версию модели. Для генерации радиоскриптов нам не нужны сложные рассуждения, достаточно связности текста.

3 Настройка Piper TTS для русского языка

Piper — один из лучших open-source синтезаторов речи, работающих локально. Для русского языка нам понадобится предобученная модель.

# Создаем директорию для моделей TTS
mkdir -p ~/.piper/models
cd ~/.piper/models

# Скачиваем русскую модель (примерно 40 МБ)
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/ru/ru_RU/ekaterina/medium/ru_RU-ekaterina-medium.onnx
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/ru/ru_RU/ekaterina/medium/ru_RU-ekaterina-medium.onnx.json

Теперь создадим простой Python-скрипт для синтеза:

# tts_synthesizer.py
import subprocess
import json
import os

class PiperTTS:
    def __init__(self, model_path, config_path):
        self.model_path = model_path
        self.config_path = config_path
        
        # Загружаем конфигурацию
        with open(config_path, 'r', encoding='utf-8') as f:
            self.config = json.load(f)
    
    def synthesize(self, text, output_path):
        """Конвертируем текст в речь"""
        # Используем piper через командную строку
        cmd = [
            'piper',
            '--model', self.model_path,
            '--config', self.config_path,
            '--output_file', output_path
        ]
        
        # Запускаем процесс
        process = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            encoding='utf-8'
        )
        
        # Передаем текст
        stdout, stderr = process.communicate(input=text)
        
        if process.returncode != 0:
            raise Exception(f"Piper error: {stderr}")
        
        return output_path

# Пример использования
if __name__ == "__main__":
    tts = PiperTTS(
        model_path="~/.piper/models/ru_RU-ekaterina-medium.onnx",
        config_path="~/.piper/models/ru_RU-ekaterina-medium.onnx.json"
    )
    
    tts.synthesize(
        "Доброе утро! Это ваша персональная радиостанция VibeCast.",
        "output.wav"
    )

Если вы хотите сравнить Piper с другими синтезаторами, посмотрите топ-6 нейросетей для синтеза речи в 2025.

4 Создание FastAPI бэкенда

Теперь объединим все компоненты в единый сервер:

# main.py
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, StreamingResponse
from pydantic import BaseModel
import subprocess
import json
import tempfile
import os
from typing import List
import asyncio

app = FastAPI(title="VibeCast Radio")

class RadioRequest(BaseModel):
    topics: List[str]
    mood: str = "friendly"
    duration_minutes: int = 5

class ScriptGenerator:
    def __init__(self):
        self.prompt_template = """Ты — ведущий радиостанции VibeCast. 
Твоя задача: создать радиоскрипт на {duration} минут.
Темы выпуска: {topics}
Настроение: {mood}

Структура скрипта:
1. Приветствие (15-20 секунд)
2. Основная часть (обсуждение тем)
3. Музыкальная пауза (упоминание)
4. Заключение

Скрипт должен быть естественным, разговорным. Не используй markdown."""
    
    async def generate(self, topics: List[str], mood: str, duration: int) -> str:
        prompt = self.prompt_template.format(
            topics=", ".join(topics),
            mood=mood,
            duration=duration
        )
        
        # Вызываем Ollama через API
        cmd = [
            "ollama", "run", "qwen:1.5b",
            prompt
        ]
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                encoding="utf-8",
                timeout=120  # 2 минуты таймаут
            )
            
            if result.returncode == 0:
                return result.stdout.strip()
            else:
                raise Exception(f"Generation failed: {result.stderr}")
        except subprocess.TimeoutExpired:
            return "Извините, генерация заняла слишком много времени. Попробуйте другие темы."

@app.post("/generate_script")
async def generate_script(request: RadioRequest):
    """Генерация радиоскрипта"""
    generator = ScriptGenerator()
    script = await generator.generate(
        request.topics,
        request.mood,
        request.duration_minutes
    )
    
    return {
        "script": script,
        "topics": request.topics,
        "duration": request.duration_minutes
    }

@app.post("/synthesize")
async def synthesize_text(text: str):
    """Синтез речи из текста"""
    # Создаем временный файл
    with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
        output_path = tmp.name
    
    # Используем Piper для синтеза
    model_path = os.path.expanduser("~/.piper/models/ru_RU-ekaterina-medium.onnx")
    config_path = os.path.expanduser("~/.piper/models/ru_RU-ekaterina-medium.onnx.json")
    
    cmd = [
        "piper",
        "--model", model_path,
        "--config", config_path,
        "--output_file", output_path
    ]
    
    process = subprocess.Popen(
        cmd,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        encoding="utf-8"
    )
    
    stdout, stderr = process.communicate(input=text)
    
    if process.returncode != 0:
        os.unlink(output_path)
        raise HTTPException(status_code=500, detail=f"Synthesis failed: {stderr}")
    
    # Возвращаем аудиофайл
    return FileResponse(
        output_path,
        media_type="audio/wav",
        filename="radio_segment.wav"
    )

@app.get("/stream_radio")
async def stream_radio():
    """Потоковая передача радио"""
    async def audio_generator():
        # Здесь можно реализовать логику непрерывного вещания
        # Например, генерация скрипта -> синтез -> отправка
        topics = ["технологии", "музыка", "новости науки"]
        generator = ScriptGenerator()
        
        while True:
            script = await generator.generate(topics, "friendly", 3)
            
            # Синтезируем отрезок
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
                output_path = tmp.name
            
            # ... синтез через Piper ...
            
            # Читаем и отправляем аудио
            with open(output_path, "rb") as audio_file:
                chunk = audio_file.read(4096)
                while chunk:
                    yield chunk
                    chunk = audio_file.read(4096)
            
            os.unlink(output_path)
            await asyncio.sleep(1)  # Пауза между отрезками
    
    return StreamingResponse(
        audio_generator(),
        media_type="audio/wav"
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
💡
Для продакшн-использования добавьте кэширование сгенерированных скриптов и аудио, чтобы не нагружать систему повторными генерациями одних и тех же тем. Также рассмотрите использование фоновых задач (Celery или аналоги) для асинхронной обработки.

5 React фронтенд для управления радиостанцией

Создадим простой интерфейс на React:

// App.jsx
import React, { useState, useEffect, useRef } from 'react';
import './App.css';

function App() {
  const [topics, setTopics] = useState(['технологии', 'искусство', 'наука']);
  const [newTopic, setNewTopic] = useState('');
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentScript, setCurrentScript] = useState('');
  const audioRef = useRef(null);
  
  const generateRadioSegment = async () => {
    try {
      const response = await fetch('http://localhost:8000/generate_script', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          topics: topics,
          mood: 'friendly',
          duration_minutes: 3
        })
      });
      
      const data = await response.json();
      setCurrentScript(data.script);
      
      // Синтезируем аудио
      const audioResponse = await fetch('http://localhost:8000/synthesize', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: data.script })
      });
      
      const audioBlob = await audioResponse.blob();
      const audioUrl = URL.createObjectURL(audioBlob);
      
      if (audioRef.current) {
        audioRef.current.src = audioUrl;
        if (isPlaying) {
          audioRef.current.play();
        }
      }
      
    } catch (error) {
      console.error('Error generating radio:', error);
    }
  };
  
  const togglePlay = () => {
    if (audioRef.current) {
      if (isPlaying) {
        audioRef.current.pause();
      } else {
        audioRef.current.play();
      }
      setIsPlaying(!isPlaying);
    }
  };
  
  const addTopic = () => {
    if (newTopic.trim()) {
      setTopics([...topics, newTopic.trim()]);
      setNewTopic('');
    }
  };
  
  useEffect(() => {
    // Автогенерация при изменении тем
    if (topics.length > 0) {
      generateRadioSegment();
    }
  }, [topics]);
  
  return (
    

🎙️ VibeCast AI Radio

Ваша персонализированная радиостанция

Темы выпуска

{topics.map((topic, index) => ( {topic} ))}
setNewTopic(e.target.value)} placeholder="Добавить тему..." onKeyPress={(e) => e.key === 'Enter' && addTopic()} />

Текущий скрипт

{currentScript || 'Скрипт будет сгенерирован автоматически...'}
); } export default App;

Запуск и настройка системы

Теперь соберем всё вместе:

# Запускаем бэкенд (в первом терминале)
cd backend
python main.py

# Запускаем фронтенд (во втором терминале)
cd frontend
npm start

# Открываем браузер по адресу:
# http://localhost:3000

Продвинутые возможности и оптимизации

1. Добавление музыкальных вставок

Реальная радиостанция не обходится без музыки. Добавим эту возможность:

# music_integration.py
import random
from pathlib import Path

class MusicLibrary:
    def __init__(self, music_dir="music"):
        self.music_dir = Path(music_dir)
        self.music_files = list(self.music_dir.glob("*.mp3")) + list(self.music_dir.glob("*.wav"))
    
    def get_random_track(self, duration_seconds=180):
        """Возвращает случайный трек (упрощенная реализация)"""
        if self.music_files:
            return random.choice(self.music_files)
        return None

# Интеграция с генератором скриптов
prompt_with_music = """...в скрипте укажи музыкальную паузу после обсуждения каждой темы..."""

2. Контекст и память между выпусками

Чтобы радиостанция "помнила", о чем говорила ранее:

class RadioMemory:
    def __init__(self, memory_file="radio_memory.json"):
        self.memory_file = memory_file
        self.previous_topics = self.load_memory()
    
    def load_memory(self):
        try:
            with open(self.memory_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except FileNotFoundError:
            return []
    
    def add_topic(self, topic, script):
        self.previous_topics.append({
            "topic": topic,
            "timestamp": datetime.now().isoformat(),
            "summary": script[:200]  # Краткое содержание
        })
        
        # Сохраняем только последние 50 тем
        if len(self.previous_topics) > 50:
            self.previous_topics = self.previous_topics[-50:]
        
        self.save_memory()
    
    def save_memory(self):
        with open(self.memory_file, 'w', encoding='utf-8') as f:
            json.dump(self.previous_topics, f, ensure_ascii=False, indent=2)
    
    def get_context(self):
        """Возвращает контекст для следующего выпуска"""
        if not self.previous_topics:
            return ""
        
        recent = self.previous_topics[-5:]  # Последние 5 тем
        context = "Ранее мы обсуждали:\n"
        for item in recent:
            context += f"- {item['topic']}: {item['summary']}\n"
        
        return context

Внимание: При работе с контекстом будьте осторожны с паразитными паттернами в LLM. Регулярно очищайте память и добавляйте guardrails для предотвращения зацикливания.

3. Оптимизация производительности

  • Кэширование аудио: Сохраняйте сгенерированные аудиофайлы с хэшем от текста, чтобы не синтезировать одно и то же повторно
  • Предзагрузка моделей: Загружайте модели TTS и LLM при старте приложения, а не при каждом запросе
  • Асинхронная генерация: Используйте фоновые задачи для подготовки следующего выпуска, пока играет текущий
  • Квантование моделей: Для Qwen 1.5B используйте 4-битное квантование (q4_0) для экономии памяти

Возможные проблемы и их решение

Проблема Причина Решение
Медленная генерация скриптов Qwen 1.5B работает на CPU Используйте GPU или перейдите на меньшую модель (Qwen:0.5b)
Роботизированный голос Базовая модель Piper Используйте более качественные модели или добавьте постобработку (reverb, pitch correction)
Повторяющийся контент Ограниченный промпт Диверсифицируйте промпты, добавьте случайные элементы
Высокая загрузка CPU Постоянная генерация Добавьте интервалы между выпусками, используйте кэширование

Идеи для развития проекта

  1. Добавление новостных лент: Интеграция с RSS для обсуждения актуальных новостей
  2. Персонализация по времени суток: Утренние выпуски бодрые, вечерние — спокойные
  3. Мультиязычность: Поддержка разных языков через соответствующие модели TTS
  4. Голосовые команды: Управление радиостанцией голосом, как в локальном голосовом ассистенте
  5. Плагинная архитектура: Возможность добавлять новые источники контента и эффекты
  6. Мобильное приложение: React Native версия для iOS/Android
🚀
VibeCast — это не просто технический эксперимент, а полноценная платформа для создания персонализированного аудиоконтента. Вы можете адаптировать её для образовательных подкастов, корпоративных радиостанций или даже для озвучки книг. Главное преимущество — полный контроль над контентом и отсутствие зависимости от внешних сервисов.

Заключение

Мы создали полностью функциональную AI-радиостанцию, которая работает локально на вашем компьютере. Ключевые преимущества этого подхода:

  • Конфиденциальность: Все данные обрабатываются локально
  • Гибкость: Можете менять модели, добавлять функции, кастомизировать под свои нужды
  • Экономия: Нет ежемесячных подписок на API
  • Образовательная ценность: Отличный проект для изучения LLM, TTS и веб-разработки

Следующим шагом может стать интеграция с системами автоматизации вроде n8n для создания сложных сценариев вещания — как описано в гайде по локальному голосовому ассистенту с n8n.

Экспериментируйте с разными моделями, добавляйте свои фичи и создавайте уникальное радио, которое будет звучать именно так, как хотите вы. Удачи в создании!