no
This commit is contained in:
@@ -5,101 +5,125 @@ import os
|
|||||||
import asyncio
|
import asyncio
|
||||||
import wave
|
import wave
|
||||||
import time
|
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...")
|
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")
|
model = WhisperModel("base", device="cpu", compute_type="int8")
|
||||||
print("✅ Model gotowy!")
|
print("✅ Model gotowy!")
|
||||||
|
|
||||||
# ============================
|
# ============================
|
||||||
# KLASA ODBIORNIKA (LIVE SINK)
|
# 2. KLASA ODBIORNIKA
|
||||||
# ============================
|
# ============================
|
||||||
class LiveSink(discord.sinks.Sink):
|
class LiveSink(discord.sinks.Sink):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.user_buffers = {} # Tu trzymamy audio w RAMie
|
self.user_buffers = {}
|
||||||
self.last_process_time = {} # Kiedy ostatnio sprawdzaliśmy usera
|
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
|
@discord.sinks.Filters.container
|
||||||
def write(self, item):
|
def write(self, data, user):
|
||||||
# Ta funkcja dostaje surowe bajty audio (PCM)
|
|
||||||
user = item.user
|
|
||||||
if user not in self.user_buffers:
|
if user not in self.user_buffers:
|
||||||
self.user_buffers[user] = bytearray()
|
self.user_buffers[user] = bytearray()
|
||||||
self.last_process_time[user] = time.time()
|
self.last_process_time[user] = time.time()
|
||||||
|
|
||||||
self.user_buffers[user] += item.data
|
self.user_buffers[user] += data
|
||||||
|
|
||||||
def get_audio_chunk(self, user):
|
def get_audio_chunk(self, user):
|
||||||
# Pobierz audio i wyczyść bufor
|
|
||||||
if user in self.user_buffers:
|
if user in self.user_buffers:
|
||||||
data = self.user_buffers[user]
|
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() # Reset bufora
|
||||||
self.user_buffers[user] = bytearray()
|
|
||||||
self.last_process_time[user] = time.time()
|
self.last_process_time[user] = time.time()
|
||||||
return data
|
return data
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ============================
|
# ============================
|
||||||
# BOT
|
# 3. LOGIKA PRZETWARZANIA
|
||||||
# ============================
|
# ============================
|
||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
intents.message_content = True
|
intents.message_content = True
|
||||||
bot = commands.Bot(command_prefix="!", intents=intents)
|
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||||
|
|
||||||
# Globalna zmienna na nasz sink
|
|
||||||
current_sink = None
|
current_sink = None
|
||||||
|
|
||||||
async def transcode_and_transcribe(user_id, pcm_data):
|
async def transcode_and_transcribe(user_id, pcm_data):
|
||||||
# 1. Zapisz surowe bajty PCM do WAV (Whisper wymaga WAV)
|
# Unikalna nazwa pliku
|
||||||
temp_filename = f"live_{user_id}.wav"
|
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:
|
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()
|
loop = asyncio.get_event_loop()
|
||||||
# Funkcja pomocnicza do uruchomienia w wątku
|
|
||||||
def run_whisper():
|
def run_whisper():
|
||||||
segments, info = model.transcribe(temp_filename, language="pl", beam_size=1)
|
segments, _ = model.transcribe(temp_filename, language="pl", beam_size=1)
|
||||||
return " ".join([segment.text for segment in segments])
|
return " ".join([s.text for s in segments])
|
||||||
|
|
||||||
text = await loop.run_in_executor(None, run_whisper)
|
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)
|
user = await bot.fetch_user(user_id)
|
||||||
print(f"🔴 LIVE {user.name}: {text}")
|
print(f"🔴 LIVE {user.name}: {text}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Błąd: {e}")
|
print(f"Błąd transkrypcji: {e}")
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(temp_filename):
|
if os.path.exists(temp_filename):
|
||||||
os.remove(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():
|
async def live_transcription_loop():
|
||||||
if current_sink is None:
|
if current_sink is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Iterujemy po kopii kluczy, bo słownik może się zmienić w trakcie
|
try:
|
||||||
for user_id in list(current_sink.user_buffers.keys()):
|
# Iterujemy po liście userów
|
||||||
# Jeśli uzbieraliśmy więcej niż 100KB danych (żeby nie mieli ciszy)
|
user_ids = list(current_sink.user_buffers.keys())
|
||||||
if len(current_sink.user_buffers[user_id]) > 150000:
|
for user_id in user_ids:
|
||||||
pcm_data = current_sink.get_audio_chunk(user_id)
|
# Sprawdzamy czy user coś mówił (czy bufor > 100KB)
|
||||||
if pcm_data:
|
if len(current_sink.user_buffers[user_id]) > 100000:
|
||||||
asyncio.create_task(transcode_and_transcribe(user_id, pcm_data))
|
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
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
print(f'🚀 Bot Live gotowy: {bot.user}')
|
print(f'🚀 Bot Live gotowy: {bot.user}')
|
||||||
@@ -121,14 +145,13 @@ async def start(ctx):
|
|||||||
print("Rozpoczynam LIVE transkrypcję...")
|
print("Rozpoczynam LIVE transkrypcję...")
|
||||||
current_sink = LiveSink()
|
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(
|
ctx.voice_client.start_recording(
|
||||||
current_sink,
|
current_sink,
|
||||||
lambda *args: None, # Callback końcowy nas nie obchodzi
|
dummy_callback,
|
||||||
ctx.channel
|
ctx.channel
|
||||||
)
|
)
|
||||||
|
|
||||||
# Odpal pętlę sprawdzającą bufor
|
|
||||||
live_transcription_loop.start()
|
live_transcription_loop.start()
|
||||||
await ctx.send("Nasłuchuję w trybie LIVE...")
|
await ctx.send("Nasłuchuję w trybie LIVE...")
|
||||||
|
|
||||||
@@ -136,10 +159,13 @@ async def start(ctx):
|
|||||||
async def stop(ctx):
|
async def stop(ctx):
|
||||||
global current_sink
|
global current_sink
|
||||||
if ctx.voice_client:
|
if ctx.voice_client:
|
||||||
ctx.voice_client.stop_recording()
|
ctx.voice_client.stop_recording() # To wywoła dummy_callback
|
||||||
live_transcription_loop.stop()
|
live_transcription_loop.stop()
|
||||||
current_sink = None
|
current_sink = None
|
||||||
await ctx.send("Zatrzymano.")
|
await ctx.send("Zatrzymano.")
|
||||||
|
|
||||||
token = os.getenv("DISCORD_TOKEN")
|
token = os.getenv("DISCORD_TOKEN")
|
||||||
bot.run(token)
|
if token:
|
||||||
|
bot.run(token)
|
||||||
|
else:
|
||||||
|
print("Brak tokena!")
|
||||||
Reference in New Issue
Block a user