diff --git a/voice_bot_live.py b/voice_bot_live.py index b2fae14..750ad3c 100644 --- a/voice_bot_live.py +++ b/voice_bot_live.py @@ -5,101 +5,125 @@ import os import asyncio import wave import time +import ctypes.util # ============================ -# KONFIGURACJA MODELU (CPU) +# 0. FIX NA OPUS (DLA PEWNOŚCI) +# ============================ +# Próba załadowania biblioteki systemowej, żeby uciszyć część błędów +opus_path = ctypes.util.find_library('opus') +if opus_path: + try: + discord.opus.load_opus(opus_path) + except: + pass + +# ============================ +# 1. KONFIGURACJA MODELU # ============================ print("⏳ Ładowanie Faster-Whisper...") -# 'tiny' jest błyskawiczny. 'base' jest mądrzejszy. -# Na CPU do live polecam 'tiny' lub 'base'. -# int8 zapewnia szybkość. model = WhisperModel("base", device="cpu", compute_type="int8") print("✅ Model gotowy!") # ============================ -# KLASA ODBIORNIKA (LIVE SINK) +# 2. KLASA ODBIORNIKA # ============================ class LiveSink(discord.sinks.Sink): def __init__(self): super().__init__() - self.user_buffers = {} # Tu trzymamy audio w RAMie - self.last_process_time = {} # Kiedy ostatnio sprawdzaliśmy usera + self.user_buffers = {} + self.last_process_time = {} + # Fix dla biblioteki py-cord (obsługa startu) + def init(self, vc): + self.vc = vc + try: + super().init(vc) + except TypeError: + pass + + # Fix dla biblioteki py-cord (obsługa zapisu danych) @discord.sinks.Filters.container - def write(self, item): - # Ta funkcja dostaje surowe bajty audio (PCM) - user = item.user + def write(self, data, user): if user not in self.user_buffers: self.user_buffers[user] = bytearray() self.last_process_time[user] = time.time() - self.user_buffers[user] += item.data + self.user_buffers[user] += data def get_audio_chunk(self, user): - # Pobierz audio i wyczyść bufor if user in self.user_buffers: data = self.user_buffers[user] - # Resetujemy bufor (tu można by zostawić kawałek dla ciągłości, ale keep it simple) - self.user_buffers[user] = bytearray() + self.user_buffers[user] = bytearray() # Reset bufora self.last_process_time[user] = time.time() return data return None # ============================ -# BOT +# 3. LOGIKA PRZETWARZANIA # ============================ intents = discord.Intents.default() intents.message_content = True bot = commands.Bot(command_prefix="!", intents=intents) -# Globalna zmienna na nasz sink current_sink = None async def transcode_and_transcribe(user_id, pcm_data): - # 1. Zapisz surowe bajty PCM do WAV (Whisper wymaga WAV) - temp_filename = f"live_{user_id}.wav" + # Unikalna nazwa pliku + temp_filename = f"live_{user_id}_{int(time.time()*1000)}.wav" - # Parametry Discorda: 48kHz, Stereo (2 kanały), 16-bit - with wave.open(temp_filename, 'wb') as wav_file: - wav_file.setnchannels(2) - wav_file.setsampwidth(2) - wav_file.setframerate(48000) - wav_file.writeframes(pcm_data) - - # 2. Transkrypcja Faster-Whisperem try: - # Uruchamiamy to w executorze, żeby nie blokować bota + # Zapis PCM do WAV (format wymagany przez Whisper) + with wave.open(temp_filename, 'wb') as wav_file: + wav_file.setnchannels(2) + wav_file.setsampwidth(2) + wav_file.setframerate(48000) + wav_file.writeframes(pcm_data) + + # Uruchomienie modelu w tle loop = asyncio.get_event_loop() - # Funkcja pomocnicza do uruchomienia w wątku def run_whisper(): - segments, info = model.transcribe(temp_filename, language="pl", beam_size=1) - return " ".join([segment.text for segment in segments]) + segments, _ = model.transcribe(temp_filename, language="pl", beam_size=1) + return " ".join([s.text for s in segments]) text = await loop.run_in_executor(None, run_whisper) - if text.strip(): + # Filtrujemy puste wiadomości i halucynacje "dzięki" + if text.strip() and "dzięki" not in text.lower(): user = await bot.fetch_user(user_id) print(f"🔴 LIVE {user.name}: {text}") except Exception as e: - print(f"Błąd: {e}") + print(f"Błąd transkrypcji: {e}") finally: if os.path.exists(temp_filename): os.remove(temp_filename) -@tasks.loop(seconds=3.0) # Sprawdzaj co 3 sekundy +@tasks.loop(seconds=3.0) async def live_transcription_loop(): if current_sink is None: return - # Iterujemy po kopii kluczy, bo słownik może się zmienić w trakcie - for user_id in list(current_sink.user_buffers.keys()): - # Jeśli uzbieraliśmy więcej niż 100KB danych (żeby nie mieli ciszy) - if len(current_sink.user_buffers[user_id]) > 150000: - pcm_data = current_sink.get_audio_chunk(user_id) - if pcm_data: - asyncio.create_task(transcode_and_transcribe(user_id, pcm_data)) + try: + # Iterujemy po liście userów + user_ids = list(current_sink.user_buffers.keys()) + for user_id in user_ids: + # Sprawdzamy czy user coś mówił (czy bufor > 100KB) + if len(current_sink.user_buffers[user_id]) > 100000: + pcm_data = current_sink.get_audio_chunk(user_id) + if pcm_data: + asyncio.create_task(transcode_and_transcribe(user_id, pcm_data)) + except Exception: + pass +# --- TO JEST NOWA FUNKCJA NAPRAWIAJĄCA BŁĄD --- +async def dummy_callback(sink, *args): + # Ta funkcja musi być async, inaczej bot wywala błąd "coroutine required" + return + +# ============================ +# 4. KOMENDY +# ============================ @bot.event async def on_ready(): print(f'🚀 Bot Live gotowy: {bot.user}') @@ -121,14 +145,13 @@ async def start(ctx): print("Rozpoczynam LIVE transkrypcję...") current_sink = LiveSink() - # Start nagrywania do naszego customowego Sinka + # Tu była lambda, która psuła bota. Teraz jest dummy_callback. ctx.voice_client.start_recording( current_sink, - lambda *args: None, # Callback końcowy nas nie obchodzi + dummy_callback, ctx.channel ) - # Odpal pętlę sprawdzającą bufor live_transcription_loop.start() await ctx.send("Nasłuchuję w trybie LIVE...") @@ -136,10 +159,13 @@ async def start(ctx): async def stop(ctx): global current_sink if ctx.voice_client: - ctx.voice_client.stop_recording() + ctx.voice_client.stop_recording() # To wywoła dummy_callback live_transcription_loop.stop() current_sink = None await ctx.send("Zatrzymano.") token = os.getenv("DISCORD_TOKEN") -bot.run(token) \ No newline at end of file +if token: + bot.run(token) +else: + print("Brak tokena!") \ No newline at end of file