How to Build a Discord Bot that Splits Audio Stems with Python (2026)

від

у

Як побудувати Discord-бота, який розділяє аудіо-стеми за допомогою Python (2026)

https://ift.tt/71R6rOb

Discord має 500 мільйонів користувачів. Більшість ботів трансльють музику з YouTube. Цей бот розділяє її на стеми — вокали, барабани, бас та інструментал — та надсилає посилання на завантаження прямо в ваші приватні повідомлення.

Ось повний покроковий посібник: slash-команда, завантаження файлу, асинхронна завантажувальна задача API, цикл опитування та результатний embed, який порадує музикантів вашого сервера.

Що ви дізнаєтеся
– ✅ Як створити slash-команду /split за допомогою discord.py 2.x app_commands
– ✅ Як приймати завантаження аудіофайлів через інтеракції Discord
– ✅ Як викликати StemSplit REST API і опитувати завершення асинхронної задачі
– ✅ Як надіслати користувачу особисто посилання на завантаження стемів у приватних повідомленнях після завершення завдання
– ✅ Як обробляти обмеження розміру файлів, помилки API та 3-секундну взаємодію Discord
– ✅ Робочий docker-compose.yml для розгортання скрізь

Необхідні речі
pip install “discord.py>=2.4” requests python-dotenv

Вам потрібно три речі:
1) Токен Discord-бота — створіть бота на discord.com/developers/applications, увімкніть наміри MESSAGE CONTENT та scope bot з дозволами Send Messages, Read Message History, Attach Files та Use Application Commands.
2) Ключ StemSplit API — отримайте один із безкоштовного тарифу на StemSplit у Developer Portal: StemSplit’s developer portal (5 хв processing, без карти). Той самий ключ живить цього бота та будь-яку іншу інтеграцію.
3) Файл .env у корені вашого проєкту:
DISCORD_TOKEN=your_discord_bot_token_here
STEMSPLIT_API_KEY=your_stemsplit_api_key_here

Як працює бот
Програма працює так:
Користувач запускає /split → завантажує аудіофайл
→ Бот відкладено відповідає (закриває 15 хвилин замість 3 секунд)
→ Відправляє файл до StemSplit API → отримує job_id
→ Пулить GET /jobs/{job_id} кожні 5 с
→ Завдання завершується → бот надсилає користувачу приватне повідомлення з embed-об’єктом з 4 посиланнями на завантаження
→ Бот відповідає публічно: “Готово! Перевірте ваші приватні повідомлення.”

StemSplit виконує HTDemucs на GPU — трек на 3 хв майже за 35–45 секунд. HTDemucs — найкраща на даний момент відкрита модель для розділення стемів, тому якість відповідає тому, що ви отримали б, запускаючи Demucs локально, без необхідності завантажувати 4 ГБ модель.

Налаштування Discord-додатку
У Discord Developer Portal:
1) Створіть новий застосунок → додайте Bot
2) У Privileged Gateway Intents увімкніть Message Content Intent
3) OAuth2 → URL Generator: виберіть scopes bot + applications.commands
4) Додайте дозволи: Send Messages, Attach Files, Use Application Commands, Send Messages in Threads
5) Скопіюйте згенерований invite URL та додайте бота на свій сервер

Повний бот (bot.py)
Ось повний файл. Спершу прочитайте, потім я розбиду кожну секцію.

“””
StemSplit Discord Bot
Splits any audio file into vocals, drums, bass, and other stems.
“””
import asyncio
import os
import time
from io import BytesIO

import discord
import requests
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()

DISCORD_TOKEN = os.environ[“DISCORD_TOKEN”]
STEMSPLIT_KEY = os.environ[“STEMSPLIT_API_KEY”]

API_BASE = “https://ift.tt/ptbIBci”
HEADERS = {“Authorization”: f”Bearer {STEMSPLIT_KEY}”}

MAX_FILE_MB = 20
POLL_INTERVAL = 5
POLL_TIMEOUT = 300 # 5 minutes max
STEM_EMOJI = {
“vocals”: “🎤”,
“drums”: “🥁”,
“bass”: “🎸”,
“other”: “🎹”,
}

class StemBot(discord.Client):
def __init__(self) -> None:
intents = discord.Intents.default()
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)

async def setup_hook(self) -> None:
await self.tree.sync()

async def on_ready(self) -> None:
print(f”Logged in as {self.user} (id: {self.user.id})”)

client = StemBot()

# ── Slash command ──────────────────────────────────────────────────────────────
@client.tree.command(name=”split”, description=”Split an audio file into stems (vocals, drums, bass, other)”)
@app_commands.describe(file=”Audio file to split (.mp3, .wav, .flac, .m4a — max 20 MB)”)
async def split_command(interaction: discord.Interaction, file: discord.Attachment) -> None:
# Defer immediately — gives us 15 minutes instead of 3 seconds
await interaction.response.defer(thinking=True)

# Validate file size
if file.size > MAX_FILE_MB * 1024 * 1024:
await interaction.followup.send(
f”❌ File too large ({file.size / 1024 / 1024:.1f} MB). Max is {MAX_FILE_MB} MB.”
)
return

# Validate file type
allowed = {“.mp3”, “.wav”, “.flac”, “.m4a”, “.ogg”, “.aac”}
ext = os.path.splitext(file.filename)[1].lower()
if ext not in allowed:
await interaction.followup.send(
f”❌ Unsupported file type `{ext}`. Supported: {‘, ‘.join(sorted(allowed))}”
)
return

await interaction.followup.send(f”⏳ Got it! Splitting **{file.filename}** — this takes ~35–45 seconds…”)

try:
# Download the attachment
audio_bytes = await file.read()

# Submit to StemSplit API
job_id = await asyncio.get_event_loop().run_in_executor(
None, lambda: submit_job(audio_bytes, file.filename)
)

# Poll until done
stems = await asyncio.get_event_loop().run_in_executor(
None, lambda: poll_job(job_id)
)

# DM the user
dm_channel = await interaction.user.create_dm()
embed = build_embed(file.filename, stems)
await dm_channel.send(embed=embed)
await interaction.followup.send(“✅ Done! Check your DMs for the stem download links.”)

except StemSplitError as e:
await interaction.followup.send(f”❌ StemSplit API error: {e}”)
except TimeoutError:
await interaction.followup.send(“❌ Job timed out after 5 minutes. Try a shorter file.”)
except Exception as e:
await interaction.followup.send(f”❌ Unexpected error: {e}”)

# ── StemSplit API helpers ──────────────────────────────────────────────────────
class StemSplitError(RuntimeError):
pass

def submit_job(audio_bytes: bytes, filename: str) -> str:
“””POST the audio file to StemSplit and return the job_id.”””
res = requests.post(
f”{API_BASE}/jobs”,
headers=HEADERS,
files={“file”: (filename, BytesIO(audio_bytes), “audio/mpeg”)},
timeout=60,
)
if not res.ok:
raise StemSplitError(
f”HTTP {res.status_code}: {res.text[:200]}”
)
return res.json()[“job_id”]

def poll_job(job_id: str) -> dict:
“””Poll GET /jobs/{job_id} every POLL_INTERVAL seconds until complete.”””
deadline = time.time() + POLL_TIMEOUT
while time.time() < deadline: res = requests.get( f"{API_BASE}/jobs/{job_id}", headers=HEADERS, timeout=15, ) if not res.ok: raise StemSplitError(f"Poll error HTTP {res.status_code}") data = res.json() status = data.get("status") if status == "done": return data.get("stems", {}) if status == "failed": raise StemSplitError(data.get("error", "Job failed")) time.sleep(POLL_INTERVAL) raise TimeoutError() def build_embed(filename: str, stems: dict) -> discord.Embed:
“””Build a Discord embed with download links for each stem.”””
embed = discord.Embed(
title=f”🎵 Stems ready: {filename}”,
description=”Your audio has been split into individual tracks. Links expire in 24 hours.”,
color=discord.Color.blurple(),
)
for stem_name, url in stems.items():
emoji = STEM_EMOJI.get(stem_name, “🎵”)
embed.add_field(
name=f”{emoji} {stem_name.capitalize()}”,
value=f”[Download {stem_name}.wav]({url})”,
inline=False,
)
embed.set_footer(text=”Powered by StemSplit · stemsplit.io”)
return embed

# ── Run ───────────────────────────────────────────────────────────────────────
client.run(DISCORD_TOKEN)

Короткі пояснення розбору коду
Deferring the interaction
Найважливіший рядок бота: await interaction.response.defer(thinking=True)
Discord надає всього 3 секунди на відповідь slash-команди; відкладення продовжує до 15 хвилин. прапорець thinking=True показує користувачу індикатор “Бот обробляє…” .

Submitting the job
POST до API_BASE/jobs з файлом; API повертає job_id миттєво. Залежні деталі дивіться в документації StemSplit.

Polling loop
Цикл опитування GET /jobs/{job_id} кожні POLL_INTERVAL секунд, поки статус не стане done або failed. Вихід з цикла — дані з stems або помилка.

The result embed
Після завершення завдання stems — словник виду {“vocals”: “…”, “drums”: “…”, …}. Ми перебираємо його та будовуємо embed з посиланнями на завантаження. Кожне посилання — пред-підписане URL, що дійсний 24 години.

Error Handling
– File size: розмір файлу обмежено 20 МБ для безпеки.
– API rate limits: StemSplit обмежує за подачею задач; для багатокористувацьких серверів можна додати cooldown на користувача.
– Discord interaction timeouts: POLL_TIMEOUT 300 секунд.

Запуск
Локально: python bot.py
Або з Docker через docker-compose (приклади в оригінальному тексті).

Wrapping Up
Бот приблизно 100 рядків, повністю асинхронний і готовий використати на будь-якому сервері. Та сама схема подачі завдання + опитування підходить для пакетної обробки, Flask API або будь-якої іншої інтеграції. Якщо хочете уникати polling, дивіться офіційний гід StemSplit для webhook-колбеків.

Якщо вам зручніше запускати модель сепарації локально без API, його варто розглянути за посиланням у статті.

Питання чи пропозиції? Залишайте коментарі — буду радий побачити, що люди різного зроблять з цим.

HI-FI News
via DEV Community https://dev.to

7 травня 2026 року, 22:18 за місцевим часом

May 7, 2026 at 10:18PM