Представьте: утренний кофе, а вместо скучных новостей или навязчивой рекламы — персонализированная радиостанция, которая говорит с вами на одном языке, обсуждает интересные именно вам темы и делает это с теплым, почти человеческим голосом. Это не фантастика, а реальный проект 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 для альтернативы
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)
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 | Постоянная генерация | Добавьте интервалы между выпусками, используйте кэширование |
Идеи для развития проекта
- Добавление новостных лент: Интеграция с RSS для обсуждения актуальных новостей
- Персонализация по времени суток: Утренние выпуски бодрые, вечерние — спокойные
- Мультиязычность: Поддержка разных языков через соответствующие модели TTS
- Голосовые команды: Управление радиостанцией голосом, как в локальном голосовом ассистенте
- Плагинная архитектура: Возможность добавлять новые источники контента и эффекты
- Мобильное приложение: React Native версия для iOS/Android
Заключение
Мы создали полностью функциональную AI-радиостанцию, которая работает локально на вашем компьютере. Ключевые преимущества этого подхода:
- Конфиденциальность: Все данные обрабатываются локально
- Гибкость: Можете менять модели, добавлять функции, кастомизировать под свои нужды
- Экономия: Нет ежемесячных подписок на API
- Образовательная ценность: Отличный проект для изучения LLM, TTS и веб-разработки
Следующим шагом может стать интеграция с системами автоматизации вроде n8n для создания сложных сценариев вещания — как описано в гайде по локальному голосовому ассистенту с n8n.
Экспериментируйте с разными моделями, добавляйте свои фичи и создавайте уникальное радио, которое будет звучать именно так, как хотите вы. Удачи в создании!