Узнайте, как создать Lo-Fi радиостанцию с генерацией сцен, музыки и видео с помощью Python, AWS и ИИ. Пошаговое руководство для разработчиков.
Вы когда-нибудь замечали, как в потоке кода время летит незаметно? Музыка часто помогает войти в это состояние. Я создал собственный Lo-Fi сервер под названием Compile & Chill, который генерирует сцены, музыку и анимации с помощью ИИ. В этой статье вы узнаете, как построить такую радиостанцию с нуля, используя Python, AWS и современные ИИ-сервисы.
Проект состоит из трёх радиостанций, каждая из которых генерирует сцену 16:9 через Amazon Bedrock, создаёт инструментальный цикл через ElevenLabs и превращает иллюстрацию в шестисекундное видео через OpenRouter. Все файлы хранятся в приватном Amazon S3 и возвращаются в браузер через Nuxt-сервер. Для управления добавлен интерфейс Stream Deck API.
Для полной сборки вам понадобятся:
Учётные данные провайдеров необязательны — без них интерфейс, встроенная сцена, переключение станций, плеер и таймер Focus Block всё равно работают.
Для безопасности используйте роль или профиль с ограниченными правами: bedrock:InvokeModel, а также разрешения на S3-бакет: s3:GetObject, s3:PutObject, s3:DeleteObject, s3:DeleteObjectVersion и s3:ListBucketVersions.
Склонируйте репозиторий и установите зависимости:
git clone https://github.com/ErikCH/compile-and-chill.git
cd compile-and-chill
npm ci
cp .env.example .env
npm run devОткройте http://127.0.0.1:8231 — вы увидите интерфейс станции с встроенной сценой.
Файл окружения разделяет функции:
ELEVENLABS_API_KEY=
BEDROCK_IMAGE_REGION=us-west-2
BEDROCK_IMAGE_MODEL_ID=stability.stable-image-ultra-v1:1
AWS_PROFILE=default
STATION_S3_BUCKET=
STATION_S3_REGION=us-west-2
OPENROUTER_API_KEY=
STATION_BIND_HOST=127.0.0.1
NUXT_CONTROL_TOKEN=Не добавляйте NUXT_PUBLIC_ к этим именам — Nuxt раскрывает публичную конфигурацию в браузерном коде. Держите STATION_BIND_HOST на loopback, если не нужен удалённый доступ. Для Stream Deck с другой машины привяжите к адресу приватной VPN, никогда к 0.0.0.0 или публичному IP, и установите длинный случайный NUXT_CONTROL_TOKEN.
Браузер не должен напрямую обращаться к провайдерам. В Compile & Chill учётные данные хранятся в runtimeConfig, а код провайдеров — в server/api/ и server/utils/.
Фрагмент nuxt.config.ts:
export default defineNuxtConfig({
runtimeConfig: {
elevenLabsApiKey: process.env.ELEVENLABS_API_KEY || '',
imageGenerationRegion: process.env.BEDROCK_IMAGE_REGION || 'us-west-2',
imageGenerationModel: process.env.BEDROCK_IMAGE_MODEL_ID || 'stability.stable-image-ultra-v1:1',
stationStorageBucket: process.env.STATION_S3_BUCKET || '',
stationStorageRegion: process.env.STATION_S3_REGION || 'us-west-2',
openRouterApiKey: process.env.OPENROUTER_API_KEY || '',
},
})Этот барьер позволяет централизованно проверять запросы, ограничивать платные параметры, редактировать подписанные URL и преобразовывать ошибки провайдеров в полезные HTTP-ответы.
Сначала аутентифицируйтесь с помощью AWS CLI:
aws sso login --profile your-profileДобавьте профиль и настройки модели в .env:
AWS_PROFILE=your-profile
BEDROCK_IMAGE_REGION=us-west-2
BEDROCK_IMAGE_MODEL_ID=stability.stable-image-ultra-v1:1Маршрут изображения проверяет режим станции, визуальный стиль, представление разработчика и опциональное зерно. Затем вызывает generateStationScene() в server/utils/station-generation.ts.
Полезная нагрузка провайдера выглядит так:
const payload = {
prompt: buildScenePrompt(mode, direction, { visualStyle, developer, diversitySeed: seed }),
negative_prompt: [
'logos, readable text, watermark, rear view, motion blur, extra fingers',
visualStyle === 'illustrated' ? 'photograph, live action, photorealistic skin, 3D render' : 'cartoon, anime, cel shading, flat illustration'
].join(', '),
mode: 'text-to-image',
aspect_ratio: '16:9',
output_format: 'png',
seed,
}
const response = await client.send(new InvokeModelCommand({
modelId: config.imageGenerationModel,
contentType: 'application/json',
accept: 'application/json',
body: JSON.stringify(payload),
}))Сервер нормализует зерно перед созданием запроса, так как Number(undefined) становится NaN, а JSON сериализует NaN как null. Stable Image Ultra ожидает целое число.
Промпт связывает визуальный стиль с типом вывода: статические сцены используют реалистичный стиль, анимированные — явно иллюстрированный 2D, чтобы источник не был фотореалистичным.
Проверка: перезапустите приложение, откройте Visual settings, выберите Realistic static и сгенерируйте одну сцену. Если модель недоступна в настроенном регионе, маршрут вернёт ошибку провайдера и идентификатор модели.
Интерфейс отправляет длительность 60 секунд, а сервер должен отклонять или нормализовать нечисловой ввод перед применением диапазона 10–60 секунд. Это защищает от отправки null провайдеру:
const requestedDuration = Number(input.durationSeconds ?? 60)
const durationSeconds = Number.isFinite(requestedDuration) ? Math.min(60, Math.max(10, requestedDuration)) : 60
const response = await fetch('https://api.elevenlabs.io/v1/music?output_format=mp3_44100_128', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'xi-api-key': config.elevenLabsApiKey,
},
body: JSON.stringify({
prompt: buildMusicPrompt(input.mode, input.direction),
music_length_ms: durationSeconds * 1000,
model_id: 'music_v2',
generation_mode: 'loop',
force_instrumental: true,
sign_with_c2pa: true,
}),
})generation_mode: 'loop' — одна из частей, делающих повтор естественным. Промпт также просит без интро, завершающей каденции или затухания. Трек, написанный как обычная песня, будет звучать сломанно при переходе от конца к первому биту.
Compile & Chill хранит один сгенерированный трек на каждый режим станции. Переключение между Deep Work и Rainy Debug повторно использует существующий трек вместо нового платного запроса.
Проверка: добавьте ELEVENLABS_API_KEY в .env, перезапустите сервер и сгенерируйте один 60-секундный трек. Переключите режимы и убедитесь, что возврат к исходному режиму использует его аудио.
Репозиторий включает AWS SAM шаблон, управляемый CloudFormation. Проверьте и разверните:
sam validate --template-file infra/compile-and-chill-private-station.yaml --region us-west-2
sam deploy --region us-west-2 --stack-name compile-and-chill-private-station --template-file infra/compile-and-chill-private-station.yaml --no-confirm-changeset --no-fail-on-empty-changesetПолучите имя медиа-бакета:
aws cloudformation describe-stacks --region us-west-2 --stack-name compile-and-chill-private-station --query "Stacks[0].Outputs[?OutputKey=='StationMediaBucketName'].OutputValue" --output textСкопируйте значение в .env:
STATION_S3_BUCKET=YOUR_STACK_OUTPUT
STATION_S3_REGION=us-west-2Стек создаёт два версионированных бакета: один для сгенерированных медиа, другой для журналов данных CloudTrail, зашифрованных KMS. CloudWatch-аларм отслеживает запросы на удаление в медиа-бакете.
Приложение назначает каждому браузерному профилю случайный station ID в HttpOnly, SameSite cookie. Объекты хранятся в структуре:
stations/<station-id>/manifest.json
stations/<station-id>/<mode>/scene.png
stations/<station-id>/<mode>/scene.mp4
stations/<station-id>/<mode>/music.mp3Браузер получает same-origin URL, например /api/library/assets/deepWork/scene. Маршрут читает объект S3 в память сервера и возвращает его. Текущая реализация — прокси, а не потоковая передача, поэтому учитывайте память при увеличении лимита видео 50 МиБ.
Проверка: перегенерируйте сцену или трек, перезагрузите страницу в том же браузерном профиле и убедитесь, что медиа возвращается. Откройте окно в приватном режиме — там будет отдельная станция.
Анимированный режим генерирует свежую иллюстрированную сцену через Amazon Bedrock, сохраняет её в S3 и создаёт 300-секундный подписанный URL. OpenRouter нужен этот временный URL, так как его видео-провайдер должен загрузить входное изображение.
Хитрость в том, чтобы отправить один и тот же якорь как ограничения первого и последнего кадра:
body: JSON.stringify({
model: modelId,
duration: 6,
resolution: '720p',
aspect_ratio: '16:9',
generate_audio: false,
prompt: [
'Animate this 2D cartoon illustration of a fictional character.',
'Use a locked-off camera and keep the illustrated composition unchanged.',
'The provided illustration is both the first and last frame.',
'Allow only tiny two-hand typing motion and one gentle blink.',
'No zoom, pan, cut, identity change, new objects, or geometry changes.',
'Create a silent seamless six-second ambient loop.'
].join(' '),
frame_images: [
{ type: 'image_url', image_url: { url: sourceUrl }, frame_type: 'first_frame' },
{ type: 'image_url', image_url: { url: sourceUrl }, frame_type: 'last_frame' }
],
})Модель может отойти от якоря, но ограничения кадров требуют возврата к той же композиции в конце. Промпт ограничивает движение небольшим движением рук и одним морганием, без движения камеры, перекадрирования, склеек, новых объектов или изменения личности.
Приложение определяет совместимые видео-модели в рантайме. Кандидат должен поддерживать 6 секунд, 720p, first_frame и last_frame. Предпочтительна Seedance 2.0 Fast, затем проверяется короткий список запасных.
Политика отказов намеренно узкая:
Генерация видео занимает минуты, а не секунды, и цены меняются. Относитесь к запросу как к платному фоновому заданию, даже если первая версия работает в одном длинном HTTP-запросе.
Длинные запросы к ИИ-провайдерам могут занимать несколько минут. Чтобы пользователь не думал, что приложение зависло, реализуйте прогресс через Server-Sent Events или WebSocket. Например, при генерации видео можно отправлять события о статусе: started, image_ready, video_started, video_completed. Это улучшает UX и позволяет отображать индикатор прогресса.
Теперь вы знаете, как создать собственную Lo-Fi радиостанцию с ИИ. Начните с клонирования репозитория, запустите базовую версию без ключей, затем постепенно добавляйте генерацию сцен, музыки и видео. Не забывайте про безопасность: храните ключи на сервере, ограничивайте права AWS и используйте подписанные URL.
Попробуйте прямо сейчас: склонируйте проект, запустите npm run dev и поэкспериментируйте с настройками визуального стиля. Затем добавьте свой ключ ElevenLabs и сгенерируйте первый трек. Удачи в творчестве!
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →