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.