2025, Nov 03 03:03
Нет discord.sinks в discord.py: как исправить и запустить MP3Sink
Ошибка ModuleNotFoundError: нет discord.sinks в discord.py. Показываем решение: перейти на py-cord[voice], включить MP3Sink и запустить голосовой переводчик.
Создать голосовой переводчик для Discord кажется простым, пока не упираешься в захват аудио. Типичный симптом — ошибка импорта, связанная с discord.sinks при использовании discord.py, из‑за неё весь конвейер останавливается ещё до того, как успеют запуститься ASR, MT и TTS. Ниже — почему возникает этот рассинхрон и как его исправить, не переписывая всю логику.
Суть проблемы
Задача: получать голос из канала, распознавать его в Whisper, переводить через NLLB и воспроизводить синтезированную речь. Реализация опиралась на MP3Sink из discord.sinks, но при установке с discord.py выполнение падало уже на этапе импорта с ModuleNotFoundError.
ModuleNotFoundError: No module named 'discord.sinks'
Минимальный пример, воспроизводящий проблему
Ниже — код, который точно воспроизводит ошибку. Он пытается импортировать MP3Sink из discord.sinks и начать запись после подключения к голосовому каналу.
import os
import asyncio
import logging
from io import BytesIO
import discord
from discord.ext import commands
from discord.sinks import MP3Sink
from transformers import (
WhisperForConditionalGeneration,
WhisperProcessor,
AutoTokenizer,
AutoModelForSeq2SeqLM
)
from gtts import gTTS
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
BOT_TOKEN = "---------------------"
VOICE_CHAN_ID = 937370496989806607
TEXT_CHAN_ID = 937370496989806605
WHISPER_NAME = "openai/whisper-large-v2"
NLLB_NAME = "facebook/nllb-200-distilled-600M"
SRC_LANG = "eng_Latn"
DST_LANG = "rus_Cyrl"
class ChannelTranslator(commands.Bot):
def __init__(self, **kwargs):
intents = discord.Intents.all()
super().__init__(command_prefix='!', intents=intents)
self.vc = None
self.asr_processor = None
self.asr_model = None
self.mt_tokenizer = None
self.mt_model = None
self.buf_queue = asyncio.Queue()
async def setup_hook(self):
await self._load_nlp()
await self._join_voice()
self.loop.create_task(self._audio_loop())
async def _load_nlp(self):
log.info("Loading models...")
self.asr_processor = WhisperProcessor.from_pretrained(WHISPER_NAME)
self.asr_model = WhisperForConditionalGeneration.from_pretrained(WHISPER_NAME).to('cuda')
self.mt_tokenizer = AutoTokenizer.from_pretrained(NLLB_NAME)
self.mt_model = AutoModelForSeq2SeqLM.from_pretrained(NLLB_NAME).to('cuda')
log.info("Models ready.")
async def _join_voice(self):
ch = self.get_channel(VOICE_CHAN_ID)
if ch:
self.vc = await ch.connect()
self.vc.start_recording(
MP3Sink(),
self._on_chunk,
self.loop
)
log.info("Connected and recording started.")
else:
log.warning("Voice channel not found!")
async def _on_chunk(self, sink, payloads, *args):
for uid, pkt in payloads.items():
if uid != self.user.id:
await self.buf_queue.put(pkt.file.read())
async def _audio_loop(self):
while True:
pcm = await self.buf_queue.get()
try:
text = await self._asr(pcm)
if text.strip():
await self._translate_and_say(text)
except Exception as ex:
log.error(f"Audio handling error: {ex}")
async def _asr(self, wav_bytes):
from transformers import pipeline
tmp = "temp_input.wav"
with open(tmp, "wb") as fh:
fh.write(wav_bytes)
pipe = pipeline(
"automatic-speech-recognition",
model=self.asr_model,
tokenizer=self.asr_processor,
device=0
)
out = pipe(tmp)
os.remove(tmp)
return out['text']
def _mt(self, text):
self.mt_tokenizer.src_lang = SRC_LANG
enc = self.mt_tokenizer(text, return_tensors="pt").to('cuda')
bos = self.mt_tokenizer.lang_code_to_id[DST_LANG]
ids = self.mt_model.generate(**enc, forced_bos_token_id=bos)
return self.mt_tokenizer.batch_decode(ids, skip_special_tokens=True)[0]
def _tts(self, text, lang='ru'):
tts = gTTS(text=text, lang=lang)
buf = BytesIO()
tts.write_to_fp(buf)
buf.seek(0)
return buf
async def _translate_and_say(self, text):
phrase = self._mt(text)
log.info(f"Translated: {phrase}")
stream = self._tts(phrase)
await self._speak(stream)
async def _speak(self, stream):
src = discord.FFmpegPCMAudio(stream, pipe=True)
if self.vc.is_playing():
self.vc.stop()
self.vc.play(src)
async def on_ready(self):
log.info(f"Logged in as {self.user}")
bot = ChannelTranslator()
bot.run(BOT_TOKEN)
Почему возникает ошибка
Импорт падает, потому что discord.sinks не входит в состав discord.py. API sinks, включая MP3Sink, доступен в pycord. Если поставить discord.py и попытаться импортировать из discord.sinks, вы получите тот самый ModuleNotFoundError. Это не ошибка вашей конфигурации — это несоответствие библиотек.
Решение
Если вам нужен discord.sinks, используйте pycord. Правильное имя пакета для установки — py-cord, не pycord, и требуются voice‑дополнения. После установки py-cord[voice] импорт начинает работать как задумано, и остальная часть конвейера продолжает выполняться.
pip install py-cord[voice]
Если вы хотите остаться на discord.py, существуют сторонние расширения для приёма аудио — например, discord-ext-audiorec и discord-ext-voice-recv. Они могут потребовать доработок кода и нередко оказываются устаревшими. Выбор зависит от того, что вам ближе: штатная поддержка sinks в pycord или адаптация внешних модулей под discord.py.
Исправленный пример на pycord
Базовая логика ниже такая же, как в проблемном фрагменте. Ключевое отличие — предполагается запуск под py-cord[voice], который и предоставляет discord.sinks.MP3Sink.
import os
import asyncio
import logging
from io import BytesIO
import discord
from discord.ext import commands
from discord.sinks import MP3Sink
from transformers import (
WhisperForConditionalGeneration,
WhisperProcessor,
AutoTokenizer,
AutoModelForSeq2SeqLM
)
from gtts import gTTS
from transformers import pipeline
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
API_TOKEN = "---------------------"
VC_ID = 937370496989806607
TC_ID = 937370496989806605
ASR_MODEL_ID = "openai/whisper-large-v2"
MT_MODEL_ID = "facebook/nllb-200-distilled-600M"
LANG_IN = "eng_Latn"
LANG_OUT = "rus_Cyrl"
class AudioRelayBot(commands.Bot):
def __init__(self, **kwargs):
intents = discord.Intents.all()
super().__init__(command_prefix='!', intents=intents)
self.vclient = None
self.whisper_tok = None
self.whisper_net = None
self.nllb_tok = None
self.nllb_net = None
self.queue_audio = asyncio.Queue()
async def setup_hook(self):
await self._boot_models()
await self._attach_voice()
self.loop.create_task(self._worker())
async def _boot_models(self):
log.info("Loading models...")
self.whisper_tok = WhisperProcessor.from_pretrained(ASR_MODEL_ID)
self.whisper_net = WhisperForConditionalGeneration.from_pretrained(ASR_MODEL_ID).to('cuda')
self.nllb_tok = AutoTokenizer.from_pretrained(MT_MODEL_ID)
self.nllb_net = AutoModelForSeq2SeqLM.from_pretrained(MT_MODEL_ID).to('cuda')
log.info("Models loaded.")
async def _attach_voice(self):
vc = self.get_channel(VC_ID)
if vc:
self.vclient = await vc.connect()
self.vclient.start_recording(
MP3Sink(),
self._on_voice,
self.loop
)
log.info("Connected and started recording.")
else:
log.warning("Voice channel not found!")
async def _on_voice(self, sink, packets, *args):
for uid, blob in packets.items():
if uid != self.user.id:
await self.queue_audio.put(blob.file.read())
async def _worker(self):
while True:
buff = await self.queue_audio.get()
try:
phrase = await self._do_asr(buff)
if phrase.strip():
await self._do_translate_and_speak(phrase)
except Exception as err:
log.error(f"Processing error: {err}")
async def _do_asr(self, raw_bytes):
tmp_name = "temp_input.wav"
with open(tmp_name, "wb") as fx:
fx.write(raw_bytes)
recog = pipeline(
"automatic-speech-recognition",
model=self.whisper_net,
tokenizer=self.whisper_tok,
device=0
)
res = recog(tmp_name)
os.remove(tmp_name)
return res['text']
def _do_translate(self, txt):
self.nllb_tok.src_lang = LANG_IN
tb = self.nllb_tok(txt, return_tensors="pt").to('cuda')
bos_id = self.nllb_tok.lang_code_to_id[LANG_OUT]
out_ids = self.nllb_net.generate(**tb, forced_bos_token_id=bos_id)
return self.nllb_tok.batch_decode(out_ids, skip_special_tokens=True)[0]
def _do_tts(self, txt, lang='ru'):
t = gTTS(text=txt, lang=lang)
bio = BytesIO()
t.write_to_fp(bio)
bio.seek(0)
return bio
async def _do_translate_and_speak(self, txt):
translated = self._do_translate(txt)
log.info(f"Translation: {translated}")
pcm = self._do_tts(translated)
await self._play_audio(pcm)
async def _play_audio(self, fp):
stream = discord.FFmpegPCMAudio(fp, pipe=True)
if self.vclient.is_playing():
self.vclient.stop()
self.vclient.play(stream)
async def on_ready(self):
log.info(f"Bot online as {self.user}")
app = AudioRelayBot()
app.run(API_TOKEN)
О сообщении «Command not found» в логах
После смены библиотеки может всплыть другая проблема: сообщение Command «join» is not found. В логах видно, что бот подключён, но команду join он отвергает. Это указывает не на аудиостек, а на отсутствие регистрации команды. Если команды объявлены внутри классов, их нужно корректно зарегистрировать, чтобы фреймворк их «увидел». Эту задачу следует решать отдельно от темы sinks.
Почему это важно
Приём голоса, sinks и запись реализованы не во всех Python‑библиотеках для Discord. Если опираться на API, которого нет в установленном пакете, вы гарантированно получите ошибки времени выполнения наподобие «модуль не найден». Чётко определитесь с экосистемой — discord.py с внешними расширениями или pycord с встроенными sinks — это сэкономит часы отладки и убережёт от неявных зависимостей на недокументированное поведение.
Итоги
Если вам нужны discord.sinks и MP3Sink, установите py-cord с voice‑дополнениями и продолжайте использовать ту же логику записи. Если хотите остаться на discord.py, рассмотрите внешние модули вроде discord-ext-audiorec или discord-ext-voice-recv, помня, что им могут понадобиться доработки и они могут быть неактуальны. Вопросы регистрации команд рассматривайте отдельно от захвата аудио и всегда сохраняйте полные тексты ошибок — нередко именно одна строка указывает точный источник проблемы.
Статья основана на вопросе на StackOverflow от Кирилл Скляров и ответе furas.