Microsoft Windows [Version 10.0.26200.9168] (c) Ко...
बनाया गया: 7 सितंबर 2026
का उपयोग करके उत्तर दिया गया GPT-5.6 Thinking द्वारा Chat01
बनाया गया: 7 सितंबर 2026
का उपयोग करके उत्तर दिया गया GPT-5.6 Thinking द्वारा Chat01
Microsoft Windows [Version 10.0.26200.9168]
(c) Корпорация Майкрософт (Microsoft Corporation). Все права защищены.
C:\Users\mamo5>C:\Users\mamo5\discord_pr\slusht
C:\Users\mamo5>cd C:\Users\mamo5\discord_pr\slusht
[ASSIGN] Worker #1 (eblan0#0390) -> RedMouse (1105531792473198632)
2026-09-07 22:06:45,222 | INFO | discord.voice_state | Connecting to voice...
2026-09-07 22:06:45,222 | INFO | discord.voice_state | Starting voice handshake... (connection attempt 1)
2026-09-07 22:06:45,509 | INFO | discord.voice_state | Voice handshake complete. Endpoint found: c-waw06-99740a0e.discord.media:2083
2026-09-07 22:06:45,651 | INFO | discord.voice_state | Voice connection complete.
[ASSIGNED] worker=#1 | user=RedMouse (1105531792473198632) | log=C:\Users\mamo5\discord_pr\slusht\logs\RedMouse_1105531792473198632.txt
[ASSIGN] Worker #2 (eblan1#4592) -> KOT_XLEB N (1359561978661703760)
2026-09-07 22:06:45,656 | INFO | discord.voice_state | Connecting to voice...
2026-09-07 22:06:45,656 | INFO | discord.voice_state | Starting voice handshake... (connection attempt 1)
2026-09-07 22:06:46,181 | INFO | discord.voice_state | Voice handshake complete. Endpoint found: c-waw06-99740a0e.discord.media:2083
2026-09-07 22:06:46,306 | INFO | discord.voice_state | Voice connection complete.
[ASSIGNED] worker=#2 | user=KOT_XLEB N (1359561978661703760) | log=C:\Users\mamo5\discord_pr\slusht\logs\KOT_XLEB N_1359561978661703760.txt
[AUDIO SKIP] worker=2 | 0.18s слишком коротко
========================================================================
[AUDIO] worker=1 | RedMouse (1105531792473198632) | 3.62s | heard=2026-09-07 22:06:47.222 +03:00
[AUDIO LEVEL] -27.5 dBFS
[VAD WHOLE] islands=2 | keeping one phrase 0.00-3.55s | 3.55s
max_new_tokens (=80) and max_length(=200) seem to have been set. max_new_tokens will take precedence. Please refer to the documentation for more information. (https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)max_new_tokens (=169) and max_length(=200) seem to have been set. max_new_tokens will take precedence. Please refer to the documentation for more information. (https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)max_new_tokens (=80) and max_length(=200) seem to have been set. max_new_tokens will take precedence. Please refer to the documentation for more information. (https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)========================================================================
[AUDIO] worker=2 | KOT_XLEB N (1359561978661703760) | 1.08s | heard=2026-09-07 22:07:28.392 +03:00
[AUDIO LEVEL] -27.5 dBFS
[VAD WHOLE] islands=1 | keeping one phrase 0.45-1.08s | 0.63s
[LANG WHOLE] top=ru:0.237 | ka=0.000 | ru:0.24, en:0.22, ko:0.08, tr:0.06, fr:0.05, nn:0.05, es:0.05, pt:0.05
2026-09-07 22:13:43,577 | INFO | faster_whisper | Processing audio with duration 00:
это логи причём супер хуйня
import asyncio
import inspect
import io
import logging
import math
import os
import re
import threading
import time
import traceback
import wave
from dataclasses import dataclass, field
from datetime import datetime
from importlib.metadata import PackageNotFoundError, version as package_version
from pathlib import Path
from typing import Any, Optional
import discord
import numpy as np
import pycountry
import torch
from discord.ext import commands, voice_recv
from dotenv import load_dotenv
from faster_whisper import WhisperModel
from faster_whisper.audio import decode_audio
from faster_whisper.vad import VadOptions, get_speech_timestamps
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
def get_package_version(name: str) -> str:
try:
return package_version(name)
except PackageNotFoundError:
return "unknown"
except Exception:
return "unknown"
FASTER_WHISPER_VERSION = get_package_version("faster-whisper")
TRANSFORMERS_VERSION = get_package_version("transformers")
DISCORD_PY_VERSION = get_package_version("discord.py")
def supported_kwargs(callable_obj: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
"""Return only keyword arguments accepted by callable_obj."""
try:
signature = inspect.signature(callable_obj)
except (TypeError, ValueError):
return dict(kwargs)
textparameters = signature.parameters if any( parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters.values() ): return dict(kwargs) return { key: value for key, value in kwargs.items() if key in parameters }
def unsupported_kwarg_names(
callable_obj: Any,
kwargs: dict[str, Any],
) -> tuple[str, ...]:
try:
signature = inspect.signature(callable_obj)
except (TypeError, ValueError):
return tuple()
textparameters = signature.parameters if any( parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters.values() ): return tuple() return tuple( key for key in kwargs if key not in parameters )
load_dotenv()
CONTROLLER_TOKEN = os.getenv("DISCORD_TOKEN")
MAX_WORKER_TOKENS = 20
WORKER_TOKENS = [
token
for i in range(1, MAX_WORKER_TOKENS + 1)
if (token := os.getenv(f"DISCORD_TOKEN{i}"))
]
SCRIPT_DIR = Path(file).resolve().parent
LOG_DIR = Path(os.getenv("LOG_DIR", str(SCRIPT_DIR / "logs"))).resolve()
LOG_DIR.mkdir(parents=True, exist_ok=True)
SILENCE_SECONDS = float(os.getenv("SILENCE_SECONDS", "0.75"))
MIN_AUDIO_SECONDS = float(os.getenv("MIN_AUDIO_SECONDS", "0.30"))
MAX_AUDIO_SECONDS = float(os.getenv("MAX_AUDIO_SECONDS", "12.0"))
SAMPLE_RATE = 48_000
CHANNELS = 2
SAMPLE_WIDTH = 2
BYTES_PER_SECOND = SAMPLE_RATE * CHANNELS * SAMPLE_WIDTH
MIN_AUDIO_BYTES = int(BYTES_PER_SECOND * MIN_AUDIO_SECONDS)
MAX_AUDIO_BYTES = int(BYTES_PER_SECOND * MAX_AUDIO_SECONDS)
ASR_SAMPLE_RATE = 16_000
GENERAL_WHISPER_MODEL = os.getenv("GENERAL_WHISPER_MODEL", "large-v3")
GEORGIAN_WHISPER_MODEL = os.getenv(
"GEORGIAN_WHISPER_MODEL",
"LukeJacob2023/whisper-large-v3-turbo-ka-ct2-gguf",
)
USE_GEORGIAN_FINE_TUNE = os.getenv(
"USE_GEORGIAN_FINE_TUNE", "1"
).lower() not in {"0", "false", "no", "off"}
LANGUAGE_DETECTION_SEGMENTS = max(
1,
int(os.getenv("LANGUAGE_DETECTION_SEGMENTS", "3")),
)
GEORGIAN_CANDIDATE_PROB = float(
os.getenv("GEORGIAN_CANDIDATE_PROB", "0.08")
)
GEORGIAN_STRONG_PROB = float(
os.getenv("GEORGIAN_STRONG_PROB", "0.16")
)
GEORGIAN_MAX_PROB_GAP = float(
os.getenv("GEORGIAN_MAX_PROB_GAP", "0.50")
)
GEORGIAN_QUALITY_MARGIN = float(
os.getenv("GEORGIAN_QUALITY_MARGIN", "0.42")
)
GEORGIAN_CONFUSABLE_MIN_PROB = float(
os.getenv("GEORGIAN_CONFUSABLE_MIN_PROB", "0.02")
)
GEORGIAN_MIN_SCRIPT_RATIO = float(
os.getenv("GEORGIAN_MIN_SCRIPT_RATIO", "0.45")
)
GEORGIAN_STRONG_WORD_PROB = float(
os.getenv("GEORGIAN_STRONG_WORD_PROB", "0.58")
)
GEORGIAN_STRONG_LOGPROB = float(
os.getenv("GEORGIAN_STRONG_LOGPROB", "-0.85")
)
GEORGIAN_CONFUSABLE_LANGS = {
code.strip().lower()
for code in os.getenv(
"GEORGIAN_CONFUSABLE_LANGS",
"fr,he,ar,fa,hy,el,tr,az",
).split(",")
if code.strip()
}
VAD_THRESHOLD = float(os.getenv("VAD_THRESHOLD", "0.58"))
VAD_MIN_SPEECH_MS = int(os.getenv("VAD_MIN_SPEECH_MS", "180"))
VAD_MIN_SILENCE_MS = int(os.getenv("VAD_MIN_SILENCE_MS", "180"))
VAD_SPEECH_PAD_MS = int(os.getenv("VAD_SPEECH_PAD_MS", "160"))
MIN_AUDIO_DBFS = float(os.getenv("MIN_AUDIO_DBFS", "-54.0"))
ASR_MIN_AVG_LOGPROB = float(os.getenv("ASR_MIN_AVG_LOGPROB", "-1.10"))
ASR_MAX_NO_SPEECH_PROB = float(os.getenv("ASR_MAX_NO_SPEECH_PROB", "0.76"))
ASR_MIN_WORD_PROB = float(os.getenv("ASR_MIN_WORD_PROB", "0.30"))
ASR_LOW_LANGUAGE_PROB = float(os.getenv("ASR_LOW_LANGUAGE_PROB", "0.18"))
ASR_LOW_LANG_MAX_CHARS = int(os.getenv("ASR_LOW_LANG_MAX_CHARS", "12"))
NLLB_MODEL_NAME = os.getenv(
"NLLB_MODEL_NAME",
"facebook/nllb-200-distilled-600M",
)
TARGET_NLLB_LANGUAGE = "rus_Cyrl"
NLLB_MAX_INPUT_TOKENS = max(
32,
int(os.getenv("NLLB_MAX_INPUT_TOKENS", "280")),
)
NLLB_MAX_NEW_TOKENS = max(
32,
int(os.getenv("NLLB_MAX_NEW_TOKENS", "420")),
)
if not CONTROLLER_TOKEN:
raise RuntimeError("Не найден DISCORD_TOKEN в .env")
if not WORKER_TOKENS:
raise RuntimeError(
"Не найдено ни одного worker token: "
"добавь DISCORD_TOKEN1 ... DISCORD_TOKEN20 в .env"
)
if not 0.0 < VAD_THRESHOLD < 1.0:
raise ValueError("VAD_THRESHOLD должен быть между 0 и 1")
if MIN_AUDIO_SECONDS <= 0:
raise ValueError("MIN_AUDIO_SECONDS должен быть > 0")
if MAX_AUDIO_SECONDS <= MIN_AUDIO_SECONDS:
raise ValueError("MAX_AUDIO_SECONDS должен быть больше MIN_AUDIO_SECONDS")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logging.getLogger("discord.ext.voice_recv.reader").setLevel(logging.WARNING)
logging.getLogger("discord.ext.voice_recv.gateway").setLevel(logging.WARNING)
FILE_LOG_LOCK = threading.Lock()
def sanitize_filename(value: str) -> str:
value = value.strip()
value = re.sub(r"[\/:*?"<>|\x00-\x1f]", "_", value)
value = re.sub(r"\s+", " ", value).strip(" .")
if not value:
value = "user"
return value[:80]
def format_heard_timestamp(dt: datetime) -> str:
local_dt = dt.astimezone()
# Milliseconds + explicit UTC offset.
base = local_dt.strftime("%Y-%m-%d %H:%M:%S")
millis = local_dt.microsecond // 1000
offset = local_dt.strftime("%z")
if len(offset) == 5:
offset = f"{offset[:3]}:{offset[3:]}"
return f"{base}.{millis:03d} {offset}"
CPU_COUNT = max(1, os.cpu_count() or 1)
CPU_THREAD_BUDGET_PERCENT = max(
10.0,
min(100.0, float(os.getenv("CPU_THREAD_BUDGET_PERCENT", "65"))),
)
CPU_THREAD_BUDGET = max(
1,
round(CPU_COUNT * CPU_THREAD_BUDGET_PERCENT / 100.0),
)
if torch.cuda.is_available():
WHISPER_DEVICE = "cuda"
WHISPER_COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "float16")
NLLB_DEVICE = "cuda"
DEFAULT_AI_PARALLELISM = 2
else:
WHISPER_DEVICE = "cpu"
WHISPER_COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "int8")
NLLB_DEVICE = "cpu"
DEFAULT_AI_PARALLELISM = 2
AI_PARALLELISM = max(
1,
int(os.getenv("AI_PARALLELISM", str(DEFAULT_AI_PARALLELISM))),
)
DEFAULT_WHISPER_CPU_THREADS = max(
1,
CPU_THREAD_BUDGET // AI_PARALLELISM,
)
WHISPER_CPU_THREADS = max(
1,
int(os.getenv("WHISPER_CPU_THREADS", str(DEFAULT_WHISPER_CPU_THREADS))),
)
TORCH_NUM_THREADS = max(
1,
int(os.getenv("TORCH_NUM_THREADS", str(CPU_THREAD_BUDGET))),
)
try:
torch.set_num_threads(TORCH_NUM_THREADS)
except RuntimeError:
pass
try:
torch.set_num_interop_threads(1)
except RuntimeError:
pass
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
VAD_REQUESTED_KWARGS: dict[str, Any] = {
"threshold": VAD_THRESHOLD,
"min_speech_duration_ms": VAD_MIN_SPEECH_MS,
"min_silence_duration_ms": VAD_MIN_SILENCE_MS,
"speech_pad_ms": VAD_SPEECH_PAD_MS,
}
VAD_UNSUPPORTED_OPTIONS = unsupported_kwarg_names(
VadOptions,
VAD_REQUESTED_KWARGS,
)
def build_vad_options() -> VadOptions:
kwargs = supported_kwargs(VadOptions, VAD_REQUESTED_KWARGS)
return VadOptions(**kwargs)
def run_get_speech_timestamps(
audio: np.ndarray,
vad_options: VadOptions,
) -> list[dict]:
kwargs = supported_kwargs(
get_speech_timestamps,
{"sampling_rate": ASR_SAMPLE_RATE},
)
return list(get_speech_timestamps(audio, vad_options, **kwargs))
print("=" * 72)
print("Загрузка локальных AI-моделей...")
print("=" * 72)
print(
f"[VERSIONS] faster-whisper={FASTER_WHISPER_VERSION} | "
f"transformers={TRANSFORMERS_VERSION} | "
f"discord.py={DISCORD_PY_VERSION} | "
f"torch={torch.version}"
)
if VAD_UNSUPPORTED_OPTIONS:
print(
"[VAD COMPAT] Эта версия faster-whisper не поддерживает: "
+ ", ".join(VAD_UNSUPPORTED_OPTIONS)
+ ". Параметры будут автоматически пропущены."
)
if torch.cuda.is_available():
print(f"[AI] CUDA: {torch.cuda.get_device_name(0)}")
else:
print("[AI] CUDA не найдена, использую CPU")
print(
f"[CPU] cores={CPU_COUNT} | thread budget={CPU_THREAD_BUDGET}/"
f"{CPU_COUNT} (~{CPU_THREAD_BUDGET_PERCENT:.0f}%) | "
f"whisper_threads/job={WHISPER_CPU_THREADS} | "
f"torch_threads={TORCH_NUM_THREADS}"
)
print(
f"[AI] General Whisper: {GENERAL_WHISPER_MODEL} | "
f"device={WHISPER_DEVICE} | compute={WHISPER_COMPUTE_TYPE} | "
f"workers={AI_PARALLELISM}"
)
general_whisper = WhisperModel(
GENERAL_WHISPER_MODEL,
device=WHISPER_DEVICE,
compute_type=WHISPER_COMPUTE_TYPE,
num_workers=AI_PARALLELISM,
cpu_threads=WHISPER_CPU_THREADS,
)
georgian_whisper: Optional[WhisperModel] = None
if USE_GEORGIAN_FINE_TUNE:
print(f"[AI] Georgian fine-tune: {GEORGIAN_WHISPER_MODEL}")
georgian_whisper = WhisperModel(
GEORGIAN_WHISPER_MODEL,
device=WHISPER_DEVICE,
compute_type=WHISPER_COMPUTE_TYPE,
num_workers=AI_PARALLELISM,
cpu_threads=WHISPER_CPU_THREADS,
)
else:
print("[AI] Georgian fine-tune отключён")
print(f"[AI] NLLB: {NLLB_MODEL_NAME} | device={NLLB_DEVICE}")
nllb_tokenizer = AutoTokenizer.from_pretrained(NLLB_MODEL_NAME)
if NLLB_DEVICE == "cuda":
nllb_model = AutoModelForSeq2SeqLM.from_pretrained(
NLLB_MODEL_NAME,
torch_dtype=torch.float16,
).to("cuda")
else:
nllb_model = AutoModelForSeq2SeqLM.from_pretrained(
NLLB_MODEL_NAME,
).to("cpu")
nllb_model.eval()
TARGET_LANGUAGE_ID = nllb_tokenizer.convert_tokens_to_ids(TARGET_NLLB_LANGUAGE)
if (
TARGET_LANGUAGE_ID is None
or TARGET_LANGUAGE_ID == nllb_tokenizer.unk_token_id
):
raise RuntimeError(
f"NLLB tokenizer не знает target language {TARGET_NLLB_LANGUAGE}"
)
NLLB_LOCK = threading.Lock()
NLLB_TAG_RE = re.compile(r"^[a-z]{3}_[A-Z][a-z]{3}$")
_nllb_vocab_tokens = set(nllb_tokenizer.get_vocab().keys())
_nllb_vocab_tokens.update(
getattr(nllb_tokenizer, "additional_special_tokens", []) or []
)
NLLB_TAGS = {
token
for token in _nllb_vocab_tokens
if NLLB_TAG_RE.match(token)
}
print(f"[LOG] Local phrase logs: {LOG_DIR}")
print("[AI] Все модели загружены")
print("=" * 72)
LANGUAGE_NAMES_RU = {
"ka": "грузинский",
"ru": "русский",
"en": "английский",
"uk": "украинский",
"de": "немецкий",
"fr": "французский",
"es": "испанский",
"it": "итальянский",
"pt": "португальский",
"pl": "польский",
"tr": "турецкий",
"el": "греческий",
"hy": "армянский",
"az": "азербайджанский",
"ar": "арабский",
"fa": "персидский",
"he": "иврит",
"hi": "хинди",
"ur": "урду",
"zh": "китайский",
"yue": "кантонский",
"ja": "японский",
"ko": "корейский",
"cs": "чешский",
"sk": "словацкий",
"sl": "словенский",
"hr": "хорватский",
"bs": "боснийский",
"sr": "сербский",
"bg": "болгарский",
"mk": "македонский",
"be": "белорусский",
"ro": "румынский",
"hu": "венгерский",
"nl": "нидерландский",
"sv": "шведский",
"da": "датский",
"fi": "финский",
"no": "норвежский",
"nn": "норвежский нюнорск",
"et": "эстонский",
"lv": "латышский",
"lt": "литовский",
"id": "индонезийский",
"ms": "малайский",
"vi": "вьетнамский",
"th": "тайский",
"bn": "бенгальский",
"ta": "тамильский",
"te": "телугу",
"kn": "каннада",
"ml": "малаялам",
"gu": "гуджарати",
"pa": "панджаби",
"ne": "непальский",
"si": "сингальский",
"my": "бирманский",
"km": "кхмерский",
"lo": "лаосский",
"sw": "суахили",
"af": "африкаанс",
"ca": "каталанский",
"eu": "баскский",
"gl": "галисийский",
"cy": "валлийский",
"is": "исландский",
"sq": "албанский",
"kk": "казахский",
"uz": "узбекский",
"tg": "таджикский",
"mn": "монгольский",
}
WHISPER_TO_NLLB = {
"en": "eng_Latn",
"ru": "rus_Cyrl",
"ka": "kat_Geor",
"uk": "ukr_Cyrl",
"de": "deu_Latn",
"fr": "fra_Latn",
"es": "spa_Latn",
"it": "ita_Latn",
"pt": "por_Latn",
"pl": "pol_Latn",
"tr": "tur_Latn",
"el": "ell_Grek",
"hy": "hye_Armn",
"he": "heb_Hebr",
"ar": "arb_Arab",
"fa": "pes_Arab",
"hi": "hin_Deva",
"ur": "urd_Arab",
"zh": "zho_Hans",
"yue": "yue_Hant",
"ja": "jpn_Jpan",
"ko": "kor_Hang",
"cs": "ces_Latn",
"sk": "slk_Latn",
"sl": "slv_Latn",
"hr": "hrv_Latn",
"bs": "bos_Latn",
"sr": "srp_Cyrl",
"bg": "bul_Cyrl",
"mk": "mkd_Cyrl",
"be": "bel_Cyrl",
"ro": "ron_Latn",
"hu": "hun_Latn",
"nl": "nld_Latn",
"sv": "swe_Latn",
"da": "dan_Latn",
"fi": "fin_Latn",
"no": "nob_Latn",
"nn": "nno_Latn",
"et": "est_Latn",
"lv": "lvs_Latn",
"lt": "lit_Latn",
"id": "ind_Latn",
"ms": "zsm_Latn",
"vi": "vie_Latn",
"th": "tha_Thai",
"bn": "ben_Beng",
"ta": "tam_Taml",
"te": "tel_Telu",
"kn": "kan_Knda",
"ml": "mal_Mlym",
"gu": "guj_Gujr",
"pa": "pan_Guru",
"ne": "npi_Deva",
"si": "sin_Sinh",
"my": "mya_Mymr",
"km": "khm_Khmr",
"lo": "lao_Laoo",
"sw": "swh_Latn",
"af": "afr_Latn",
"ca": "cat_Latn",
"eu": "eus_Latn",
"gl": "glg_Latn",
"cy": "cym_Latn",
"is": "isl_Latn",
"sq": "als_Latn",
"kk": "kaz_Cyrl",
"uz": "uzn_Latn",
"tg": "tgk_Cyrl",
"mn": "khk_Cyrl",
"az": "azj_Latn",
"jw": "jav_Latn",
"jv": "jav_Latn",
"yi": "ydd_Hebr",
"ht": "hat_Latn",
"mi": "mri_Latn",
}
ISO_ALIASES = {
"jw": "jv",
"iw": "he",
}
def language_name_ru(code: str) -> str:
if code in LANGUAGE_NAMES_RU:
return LANGUAGE_NAMES_RU[code]
textlookup_code = ISO_ALIASES.get(code, code) try: if len(lookup_code) == 2: obj = pycountry.languages.get(alpha_2=lookup_code) else: obj = pycountry.languages.get(alpha_3=lookup_code) if obj and getattr(obj, "name", None): return obj.name except Exception: pass return code.upper()
def georgian_char_count(text: str) -> int:
return sum("\u10A0" <= ch <= "\u10FF" for ch in text)
def contains_georgian(text: str) -> bool:
return georgian_char_count(text) >= 1
def georgian_script_ratio(text: str) -> float:
letters = [ch for ch in text if ch.isalpha()]
if not letters:
return 0.0
return georgian_char_count(text) / len(letters)
def contains_cyrillic(text: str) -> bool:
return any("\u0400" <= ch <= "\u052F" for ch in text)
def detect_text_script(text: str) -> Optional[str]:
ranges = {
"Geor": ("\u10A0", "\u10FF"),
"Cyrl": ("\u0400", "\u052F"),
"Arab": ("\u0600", "\u06FF"),
"Hebr": ("\u0590", "\u05FF"),
"Grek": ("\u0370", "\u03FF"),
"Deva": ("\u0900", "\u097F"),
"Beng": ("\u0980", "\u09FF"),
"Guru": ("\u0A00", "\u0A7F"),
"Gujr": ("\u0A80", "\u0AFF"),
"Taml": ("\u0B80", "\u0BFF"),
"Telu": ("\u0C00", "\u0C7F"),
"Knda": ("\u0C80", "\u0CFF"),
"Mlym": ("\u0D00", "\u0D7F"),
"Sinh": ("\u0D80", "\u0DFF"),
"Thai": ("\u0E00", "\u0E7F"),
"Laoo": ("\u0E80", "\u0EFF"),
"Mymr": ("\u1000", "\u109F"),
"Khmr": ("\u1780", "\u17FF"),
"Hang": ("\uAC00", "\uD7AF"),
}
textscores = { script: sum(lo <= ch <= hi for ch in text) for script, (lo, hi) in ranges.items() } if any("\u3040" <= ch <= "\u30FF" for ch in text): scores["Jpan"] = scores.get("Jpan", 0) + 5 if any("\u4E00" <= ch <= "\u9FFF" for ch in text): scores["Hans"] = scores.get("Hans", 0) + 4 best_script = max(scores, key=scores.get, default=None) if best_script and scores[best_script] > 0: return best_script if any(ch.isalpha() and ord(ch) < 0x0250 for ch in text): return "Latn" return None
def resolve_nllb_language(
whisper_code: str,
text: str,
) -> Optional[str]:
explicit = WHISPER_TO_NLLB.get(whisper_code)
if explicit:
token_id = nllb_tokenizer.convert_tokens_to_ids(explicit)
if token_id is not None and token_id != nllb_tokenizer.unk_token_id:
return explicit
textcode = ISO_ALIASES.get(whisper_code, whisper_code) alpha3 = None try: if len(code) == 2: obj = pycountry.languages.get(alpha_2=code) alpha3 = getattr(obj, "alpha_3", None) if obj else None elif len(code) == 3: alpha3 = code except Exception: alpha3 = None if not alpha3: return None candidates = sorted( tag for tag in NLLB_TAGS if tag.startswith(alpha3 + "_") ) if not candidates: return None if len(candidates) == 1: return candidates[0] script = detect_text_script(text) if script: exact = [ tag for tag in candidates if tag.endswith("_" + script) ] if exact: return exact[0] return candidates[0]
@dataclass(frozen=True)
class LanguageDetection:
language: str
probability: float
all_probs: tuple[tuple[str, float], ...]
textdef probability_for(self, code: str) -> float: for lang, prob in self.all_probs: if lang == code: return float(prob) return 0.0
@dataclass(frozen=True)
class TranscriptionPiece:
text: str
language: str
probability: float
asr_model: str
start: float
end: float
avg_logprob: float
no_speech_prob: float
avg_word_prob: float
@dataclass(frozen=True)
class TranscriptionResult:
pieces: tuple[TranscriptionPiece, ...]
text@property def text(self) -> str: return " ".join( piece.text.strip() for piece in self.pieces if piece.text.strip() ).strip() @property def languages(self) -> tuple[str, ...]: seen: list[str] = [] for piece in self.pieces: if piece.language not in seen: seen.append(piece.language) return tuple(seen)
@dataclass(frozen=True)
class ASRCandidate:
text: str
segments: tuple
avg_logprob: float
no_speech_prob: float
avg_word_prob: float
valid: bool
quality: float
reject_reason: str = ""
@dataclass(frozen=True)
class CapturedPhrase:
pcm: bytes
heard_at: datetime
ended_at: datetime
@dataclass(frozen=True)
class PreparedAudio:
audio: np.ndarray
lid_audio: np.ndarray
crop_start: float
crop_end: float
dbfs: float
def pcm_to_wav(pcm: bytes) -> io.BytesIO:
output = io.BytesIO()
textwith wave.open(output, "wb") as wav: wav.setnchannels(CHANNELS) wav.setsampwidth(SAMPLE_WIDTH) wav.setframerate(SAMPLE_RATE) wav.writeframes(pcm) output.seek(0) output.name = "discord_speech.wav" return output
def pcm_to_16k_float(pcm: bytes) -> np.ndarray:
wav_file = pcm_to_wav(pcm)
audio = decode_audio(wav_file, sampling_rate=ASR_SAMPLE_RATE)
audio = np.asarray(audio, dtype=np.float32)
textif audio.ndim > 1: audio = np.mean(audio, axis=0, dtype=np.float32) return np.ascontiguousarray(audio.reshape(-1), dtype=np.float32)
def dbfs(audio: np.ndarray) -> float:
if audio.size == 0:
return -120.0
textrms = float( np.sqrt( np.mean(np.square(audio, dtype=np.float64)) ) ) if rms <= 1e-9: return -120.0 return 20.0 * math.log10(rms)
def prepare_whole_phrase_audio(pcm: bytes) -> Optional[PreparedAudio]:
"""
Convert to 16 kHz mono and use Silero only as a speech gate / edge crop.
textWe DO NOT transcribe individual VAD timestamps anymore. If VAD finds more than one speech island, everything from the first start to the last end is kept as one continuous phrase so language detection and ASR get context. """ audio = pcm_to_16k_float(pcm) if audio.size == 0: return None full_dbfs = dbfs(audio) print(f"[AUDIO LEVEL] {full_dbfs:.1f} dBFS") if full_dbfs < MIN_AUDIO_DBFS: print( f"[NOISE DROP/FULL] {full_dbfs:.1f} dBFS < " f"{MIN_AUDIO_DBFS:.1f} dBFS" ) return None timestamps = run_get_speech_timestamps(audio, build_vad_options()) if not timestamps: print("[VAD] speech not found") return None first_start = max(0, int(timestamps[0]["start"])) last_end = min(audio.shape[0], int(timestamps[-1]["end"])) if last_end <= first_start: return None phrase = np.ascontiguousarray( audio[first_start:last_end], dtype=np.float32, ) # For language-ID only, concatenate the speech islands so pauses/noise do # not dominate the language token. ASR still receives `phrase` above as # one continuous unsegmented phrase. lid_chunks = [] for item in timestamps: start_sample = max(0, int(item["start"])) end_sample = min(audio.shape[0], int(item["end"])) if end_sample > start_sample: lid_chunks.append(audio[start_sample:end_sample]) lid_audio = ( np.ascontiguousarray(np.concatenate(lid_chunks), dtype=np.float32) if lid_chunks else phrase ) phrase_duration = phrase.shape[0] / ASR_SAMPLE_RATE phrase_dbfs = dbfs(phrase) if phrase_duration < 0.18: print(f"[VAD] cropped phrase too short: {phrase_duration:.2f}s") return None if phrase_dbfs < MIN_AUDIO_DBFS: print( f"[NOISE DROP/CROP] {phrase_dbfs:.1f} dBFS < " f"{MIN_AUDIO_DBFS:.1f} dBFS" ) return None crop_start = first_start / ASR_SAMPLE_RATE crop_end = last_end / ASR_SAMPLE_RATE print( f"[VAD WHOLE] islands={len(timestamps)} | " f"keeping one phrase {crop_start:.2f}-{crop_end:.2f}s | " f"{phrase_duration:.2f}s" ) return PreparedAudio( audio=phrase, lid_audio=lid_audio, crop_start=crop_start, crop_end=crop_end, dbfs=phrase_dbfs, )
def detect_phrase_language(audio: np.ndarray) -> LanguageDetection:
kwargs = {
"audio": audio,
"vad_filter": False,
"language_detection_threshold": 0.0,
"language_detection_segments": LANGUAGE_DETECTION_SEGMENTS,
}
textfiltered = supported_kwargs(general_whisper.detect_language, kwargs) if "audio" not in filtered: filtered["audio"] = audio try: result = general_whisper.detect_language(**filtered) if not isinstance(result, tuple): raise RuntimeError( f"Неожиданный ответ detect_language: {type(result)!r}" ) if len(result) == 3: language, probability, all_probs = result elif len(result) == 2: language, probability = result all_probs = [(language, probability)] else: raise RuntimeError( f"Неожиданное число значений detect_language: {len(result)}" ) except Exception as exc: print(f"[LANG DETECT ERROR] {type(exc).__name__}: {exc}") return LanguageDetection("unknown", 0.0, tuple()) normalized_probs: list[tuple[str, float]] = [] for item in all_probs or []: try: code, prob = item normalized_probs.append((str(code), float(prob))) except Exception: continue normalized_probs.sort(key=lambda pair: pair[1], reverse=True) return LanguageDetection( language=str(language or "unknown"), probability=float(probability or 0.0), all_probs=tuple(normalized_probs), )
def _candidate_from_segments(segments: list) -> ASRCandidate:
usable = [
segment
for segment in segments
if getattr(segment, "text", None) and segment.text.strip()
]
textif not usable: return ASRCandidate( text="", segments=tuple(), avg_logprob=-99.0, no_speech_prob=1.0, avg_word_prob=0.0, valid=False, quality=-99.0, reject_reason="empty", ) text = " ".join(segment.text.strip() for segment in usable).strip() durations = [ max( 0.05, float( getattr(segment, "end", 0.0) - getattr(segment, "start", 0.0) ), ) for segment in usable ] duration_sum = max(sum(durations), 1e-6) avg_logprob = sum( float(getattr(segment, "avg_logprob", -99.0)) * duration for segment, duration in zip(usable, durations) ) / duration_sum no_speech_prob = sum( float(getattr(segment, "no_speech_prob", 1.0)) * duration for segment, duration in zip(usable, durations) ) / duration_sum word_probs: list[float] = [] for segment in usable: for word in (getattr(segment, "words", None) or []): probability = getattr(word, "probability", None) if probability is not None: try: word_probs.append(float(probability)) except (TypeError, ValueError): pass avg_word_prob = ( sum(word_probs) / len(word_probs) if word_probs else 0.50 ) reject_reason = "" if avg_logprob < ASR_MIN_AVG_LOGPROB: reject_reason = f"avg_logprob={avg_logprob:.3f}" elif no_speech_prob > ASR_MAX_NO_SPEECH_PROB and avg_logprob < -0.35: reject_reason = f"no_speech={no_speech_prob:.3f}" elif word_probs and avg_word_prob < ASR_MIN_WORD_PROB: reject_reason = f"word_prob={avg_word_prob:.3f}" quality = ( avg_logprob + 0.70 * avg_word_prob - 0.65 * no_speech_prob ) return ASRCandidate( text=text, segments=tuple(usable), avg_logprob=avg_logprob, no_speech_prob=no_speech_prob, avg_word_prob=avg_word_prob, valid=not reject_reason, quality=quality, reject_reason=reject_reason, )
def transcribe_phrase_candidate(
model: WhisperModel,
audio: np.ndarray,
language: Optional[str],
) -> ASRCandidate:
kwargs = {
"language": None if not language or language == "unknown" else language,
"task": "transcribe",
"beam_size": 5,
"best_of": 5,
"temperature": 0.0,
"condition_on_previous_text": False,
"vad_filter": False,
"word_timestamps": True,
"hallucination_silence_threshold": 0.8,
"no_speech_threshold": 0.60,
"log_prob_threshold": -1.0,
"compression_ratio_threshold": 2.4,
"repetition_penalty": 1.05,
"no_repeat_ngram_size": 3,
}
textfiltered = supported_kwargs(model.transcribe, kwargs) segments, _ = model.transcribe(audio, **filtered) return _candidate_from_segments(list(segments))
def georgian_candidate_is_strong(candidate: ASRCandidate) -> bool:
return (
candidate.valid
and georgian_char_count(candidate.text) >= 2
and georgian_script_ratio(candidate.text) >= GEORGIAN_MIN_SCRIPT_RATIO
and candidate.avg_word_prob >= GEORGIAN_STRONG_WORD_PROB
and candidate.avg_logprob >= GEORGIAN_STRONG_LOGPROB
and candidate.no_speech_prob <= 0.65
)
def choose_phrase_transcription(
audio: np.ndarray,
detection: LanguageDetection,
) -> tuple[ASRCandidate, str, float, str, str]:
"""
Evaluate Georgian fine-tune on the WHOLE phrase every time.
textReturn candidate, language, probability, model label, decision reason. """ detected_language = detection.language detected_probability = detection.probability ka_probability = detection.probability_for("ka") georgian_candidate: Optional[ASRCandidate] = None # Georgian expert always gets a chance first and is explicitly forced to ka. if georgian_whisper is not None: georgian_candidate = transcribe_phrase_candidate( georgian_whisper, audio, "ka", ) print( f"[KA CANDIDATE] valid={georgian_candidate.valid} | " f"q={georgian_candidate.quality:.3f} | " f"logp={georgian_candidate.avg_logprob:.3f} | " f"nospeech={georgian_candidate.no_speech_prob:.3f} | " f"wordp={georgian_candidate.avg_word_prob:.3f} | " f"script={georgian_script_ratio(georgian_candidate.text):.2f} | " f"{georgian_candidate.text!r}" ) general_candidate = transcribe_phrase_candidate( general_whisper, audio, detected_language, ) print( f"[GENERAL CANDIDATE] lang={detected_language}:{detected_probability:.3f} | " f"valid={general_candidate.valid} | " f"q={general_candidate.quality:.3f} | " f"logp={general_candidate.avg_logprob:.3f} | " f"nospeech={general_candidate.no_speech_prob:.3f} | " f"wordp={general_candidate.avg_word_prob:.3f} | " f"{general_candidate.text!r}" ) if georgian_candidate is None: return ( general_candidate, detected_language, detected_probability, "large-v3-general", "georgian-model-off", ) geo_ok = ( georgian_candidate.valid and georgian_char_count(georgian_candidate.text) >= 2 and georgian_script_ratio(georgian_candidate.text) >= GEORGIAN_MIN_SCRIPT_RATIO ) geo_strong = georgian_candidate_is_strong(georgian_candidate) if detected_language == "ka" and geo_ok: return ( georgian_candidate, "ka", max(detected_probability, ka_probability, 0.50), "georgian-finetune", "language-id-ka", ) if geo_ok and not general_candidate.valid: return ( georgian_candidate, "ka", max(ka_probability, 0.50), "georgian-finetune", "general-invalid", ) probability_gap = detected_probability - ka_probability quality_close = ( georgian_candidate.quality >= general_candidate.quality - GEORGIAN_QUALITY_MARGIN ) if ( geo_ok and ka_probability >= GEORGIAN_STRONG_PROB and quality_close ): return ( georgian_candidate, "ka", max(ka_probability, 0.50), "georgian-finetune", "ka-prob-strong", ) if ( geo_ok and ka_probability >= GEORGIAN_CANDIDATE_PROB and probability_gap <= GEORGIAN_MAX_PROB_GAP and quality_close ): return ( georgian_candidate, "ka", max(ka_probability, 0.50), "georgian-finetune", "ka-prob-gap", ) # Main fix for the observed fr/he misrouting: if general LID claims one of # these commonly confused languages but the dedicated Georgian decoder is # acoustically strong on the whole phrase, prefer Georgian even when the # general language token underestimates ka. if ( geo_strong and detected_language in GEORGIAN_CONFUSABLE_LANGS and ( ka_probability >= GEORGIAN_CONFUSABLE_MIN_PROB or detected_probability < 0.65 ) and georgian_candidate.quality >= general_candidate.quality - (GEORGIAN_QUALITY_MARGIN + 0.18) ): return ( georgian_candidate, "ka", max(ka_probability, 0.50), "georgian-finetune", f"confusable-{detected_language}", ) # If general language-ID itself is not confident, a strong Georgian expert # result is more trustworthy than a weak top language guess. if ( geo_strong and detected_probability < 0.62 and georgian_candidate.quality >= general_candidate.quality - 0.25 ): return ( georgian_candidate, "ka", max(ka_probability, 0.50), "georgian-finetune", "weak-general-lid", ) return ( general_candidate, detected_language, detected_probability, "large-v3-general", "general-wins", )
def transcribe_multilingual_pcm(pcm: bytes) -> TranscriptionResult:
prepared = prepare_whole_phrase_audio(pcm)
textif prepared is None: return TranscriptionResult(tuple()) audio = prepared.audio detection = detect_phrase_language(prepared.lid_audio) top_preview = ", ".join( f"{code}:{prob:.2f}" for code, prob in detection.all_probs[:8] ) print( f"[LANG WHOLE] top={detection.language}:{detection.probability:.3f} | " f"ka={detection.probability_for('ka'):.3f} | {top_preview}" ) candidate, language, probability, asr_model, reason = ( choose_phrase_transcription(audio, detection) ) if not candidate.valid: print( f"[NOISE DROP/POST] lang={language}:{probability:.3f} | " f"reason={candidate.reject_reason} | text={candidate.text!r}" ) return TranscriptionResult(tuple()) compact_chars = len(re.sub(r"\s+", "", candidate.text)) if ( language != "ka" and probability < ASR_LOW_LANGUAGE_PROB and compact_chars <= ASR_LOW_LANG_MAX_CHARS ): print( f"[NOISE DROP/LANG] lang={language}:{probability:.3f} | " f"chars={compact_chars} | text={candidate.text!r}" ) return TranscriptionResult(tuple()) if contains_georgian(candidate.text): language = "ka" probability = max( probability, detection.probability_for("ka"), 0.50, ) piece = TranscriptionPiece( text=candidate.text, language=language, probability=max(0.0, min(1.0, probability)), asr_model=asr_model, start=prepared.crop_start, end=prepared.crop_end, avg_logprob=candidate.avg_logprob, no_speech_prob=candidate.no_speech_prob, avg_word_prob=candidate.avg_word_prob, ) print( f"[STT WHOLE] lang={piece.language}:{piece.probability:.3f} | " f"model={piece.asr_model} | decision={reason} | " f"logp={piece.avg_logprob:.3f} | " f"nospeech={piece.no_speech_prob:.3f} | " f"wordp={piece.avg_word_prob:.3f} | {piece.text}" ) return TranscriptionResult((piece,))
TOKEN_WITH_SPACE_RE = re.compile(r"\S+\s*", re.UNICODE)
def effective_token_language(token: str, default_language: str) -> str:
if contains_georgian(token):
return "ka"
textif default_language == "ka" and contains_cyrillic(token): return "ru" return default_language
def split_text_by_effective_language(
text: str,
default_language: str,
) -> list[tuple[str, str]]:
tokens = TOKEN_WITH_SPACE_RE.findall(text)
textif not tokens: return [(default_language, text)] if text else [] groups: list[tuple[str, str]] = [] current_language: Optional[str] = None current_parts: list[str] = [] for token in tokens: language = effective_token_language(token, default_language) if current_language is None: current_language = language if language != current_language: groups.append((current_language, "".join(current_parts).strip())) current_language = language current_parts = [] current_parts.append(token) if current_parts and current_language is not None: groups.append((current_language, "".join(current_parts).strip())) return [ (language, group_text) for language, group_text in groups if group_text ]
def _token_count_for_nllb(text: str) -> int:
encoded = nllb_tokenizer(
text,
add_special_tokens=True,
truncation=False,
)
return len(encoded["input_ids"])
def _split_pathological_text(text: str) -> list[str]:
if not text:
return []
textif _token_count_for_nllb(text) <= NLLB_MAX_INPUT_TOKENS: return [text] midpoint = max(1, len(text) // 2) if midpoint >= len(text): return [text] return ( _split_pathological_text(text[:midpoint]) + _split_pathological_text(text[midpoint:]) )
def _split_long_unit_by_words(unit: str) -> list[str]:
words = unit.split()
textif not words: return [] result: list[str] = [] current: list[str] = [] for word in words: candidate = " ".join(current + [word]) if current and _token_count_for_nllb(candidate) > NLLB_MAX_INPUT_TOKENS: result.append(" ".join(current)) current = [word] else: current.append(word) if current: result.append(" ".join(current)) final: list[str] = [] for part in result: if _token_count_for_nllb(part) <= NLLB_MAX_INPUT_TOKENS: final.append(part) else: final.extend(_split_pathological_text(part)) return final
def split_text_for_nllb(text: str) -> list[str]:
text = text.strip()
textif not text: return [] units = [ part.strip() for part in re.split(r"(?<=[.!?…])\s+|\n+", text) if part.strip() ] if not units: units = [text] expanded: list[str] = [] for unit in units: if _token_count_for_nllb(unit) <= NLLB_MAX_INPUT_TOKENS: expanded.append(unit) else: expanded.extend(_split_long_unit_by_words(unit)) packed: list[str] = [] current = "" for unit in expanded: candidate = f"{current} {unit}".strip() if current else unit if current and _token_count_for_nllb(candidate) > NLLB_MAX_INPUT_TOKENS: packed.append(current) current = unit else: current = candidate if current: packed.append(current) return packed
def _generate_nllb_translation(
text: str,
source_language: str,
retry: bool = False,
) -> str:
nllb_tokenizer.src_lang = source_language
textinputs = nllb_tokenizer( text, return_tensors="pt", truncation=True, max_length=NLLB_MAX_INPUT_TOKENS + 16, ) input_token_count = int(inputs["input_ids"].shape[-1]) inputs = { key: value.to(NLLB_DEVICE) for key, value in inputs.items() } dynamic_max_new = min( 512, NLLB_MAX_NEW_TOKENS, max( 80, int(input_token_count * (2.8 if retry else 2.2)) + 40, ), ) generated = nllb_model.generate( **inputs, forced_bos_token_id=TARGET_LANGUAGE_ID, max_new_tokens=dynamic_max_new, num_beams=6 if retry else 4, early_stopping=True, no_repeat_ngram_size=3, repetition_penalty=1.04, length_penalty=1.03 if retry else 1.0, ) decoded = nllb_tokenizer.batch_decode( generated, skip_special_tokens=True, ) return decoded[0].strip() if decoded else ""
@torch.inference_mode()
def translate_to_russian(text: str, whisper_language: str) -> str:
text = text.strip()
textif not text: return "" if whisper_language == "ru": return text source_language = resolve_nllb_language(whisper_language, text) if not source_language: return ( "⚠️ Не удалось подобрать NLLB-код для этого языка. " f"Оригинал: {text}" ) with NLLB_LOCK: chunks = split_text_for_nllb(text) translated_chunks: list[str] = [] for chunk in chunks: translated = _generate_nllb_translation( chunk, source_language, retry=False, ) source_compact = len(re.sub(r"\s+", "", chunk)) translated_compact = len(re.sub(r"\s+", "", translated)) if ( source_compact >= 35 and translated_compact < max(8, int(source_compact * 0.22)) ): retry_translation = _generate_nllb_translation( chunk, source_language, retry=True, ) if len(retry_translation) > len(translated): translated = retry_translation if translated: translated_chunks.append(translated) return " ".join(translated_chunks).strip()
def translate_transcription_to_russian(result: TranscriptionResult) -> str:
output_parts: list[str] = []
textfor piece in result.pieces: groups = split_text_by_effective_language(piece.text, piece.language) for language, text in groups: translated = translate_to_russian(text, language) if translated: output_parts.append(translated) return " ".join(output_parts).strip()
@dataclass(frozen=True)
class SpeakerIdentity:
user_id: int
username: str
display_name: str
text@classmethod def from_member(cls, member: discord.Member) -> "SpeakerIdentity": return cls( user_id=member.id, username=member.name, display_name=member.display_name, )
def speaker_log_path(speaker: SpeakerIdentity) -> Path:
safe_name = sanitize_filename(speaker.display_name)
return LOG_DIR / f"{safe_name}_{speaker.user_id}.txt"
def append_phrase_log(
speaker: SpeakerIdentity,
worker_number: int,
heard_at: datetime,
ended_at: datetime,
transcription: TranscriptionResult,
russian_text: str,
) -> Path:
path = speaker_log_path(speaker)
textlanguage_parts: list[str] = [] model_names: list[str] = [] for piece in transcription.pieces: language_parts.append( f"{language_name_ru(piece.language)} ({piece.language}) " f"{piece.probability * 100:.1f}%" ) if piece.asr_model not in model_names: model_names.append(piece.asr_model) heard_stamp = format_heard_timestamp(heard_at) ended_stamp = format_heard_timestamp(ended_at) entry = ( f"[{heard_stamp}]\n" f"heard_end: {ended_stamp}\n" f"user: {speaker.display_name} (@{speaker.username}, {speaker.user_id})\n" f"worker: #{worker_number}\n" f"language: {', '.join(language_parts) if language_parts else 'unknown'}\n" f"asr: {' + '.join(model_names) if model_names else 'unknown'}\n" f"original: {transcription.text}\n" f"russian: {russian_text}\n" f"{'-' * 72}\n" ) with FILE_LOG_LOCK: is_new = not path.exists() with path.open("a", encoding="utf-8") as file: if is_new: file.write( f"Discord voice translation log\n" f"display_name: {speaker.display_name}\n" f"username: {speaker.username}\n" f"user_id: {speaker.user_id}\n" f"{'=' * 72}\n" ) file.write(entry) file.flush() return path
controller_intents = discord.Intents.default()
controller_intents.message_content = True
controller_intents.voice_states = True
controller_member_cache = discord.MemberCacheFlags.none()
controller_member_cache.voice = True
bot = commands.Bot(
command_prefix="!",
intents=controller_intents,
member_cache_flags=controller_member_cache,
case_insensitive=True,
)
@dataclass
class AudioBuffer:
pcm: bytearray = field(default_factory=bytearray)
last_packet_monotonic: float = field(default_factory=time.monotonic)
heard_at: Optional[datetime] = None
ended_at: Optional[datetime] = None
class FocusedSink(voice_recv.AudioSink):
"""Keep PCM only from the worker's assigned human user."""
textdef __init__(self, worker: "VoiceWorker"): super().__init__() self.worker = worker def wants_opus(self) -> bool: return False def write( self, user: discord.Member | discord.User | None, data: voice_recv.VoiceData, ): if user is None: return target_user_id = self.worker.target_user_id if target_user_id is None or user.id != target_user_id: return if getattr(user, "bot", False): return pcm = getattr(data, "pcm", None) if not pcm: return self.worker.add_audio(pcm) def cleanup(self): pass
class VoiceWorker:
def init(self, number: int, token: str):
self.number = number
self.token = token
textintents = discord.Intents.default() intents.voice_states = True intents.message_content = False member_cache = discord.MemberCacheFlags.none() member_cache.voice = True self.client = discord.Client( intents=intents, member_cache_flags=member_cache, ) self.ready_event = asyncio.Event() self.failed_reason: Optional[str] = None self.session: Optional["VoiceTranslationSession"] = None self.voice_client: Optional[voice_recv.VoiceRecvClient] = None self.sink: Optional[FocusedSink] = None self.target: Optional[SpeakerIdentity] = None self.target_user_id: Optional[int] = None self.running = False self.audio = AudioBuffer() self.audio_lock = threading.Lock() self.flusher_task: Optional[asyncio.Task] = None self._install_events() @property def is_ready(self) -> bool: return self.client.is_ready() and self.failed_reason is None @property def is_busy(self) -> bool: return self.target_user_id is not None @property def label(self) -> str: user = self.client.user if user: return f"Worker #{self.number} ({user})" return f"Worker #{self.number}" def _install_events(self): @self.client.event async def on_ready(): print(f"[WORKER READY] #{self.number}: {self.client.user}") self.ready_event.set() @self.client.event async def on_voice_state_update(member, before, after): if self.client.user is None or member.id != self.client.user.id: return if after.deaf and not before.deaf: print(f"[WORKER WARNING] #{self.number} server-deafened") async def run_client(self): try: await self.client.start(self.token, reconnect=True) except Exception as exc: self.failed_reason = f"{type(exc).__name__}: {exc}" self.ready_event.set() print( f"[WORKER LOGIN ERROR] #{self.number}: " f"{self.failed_reason}" ) async def connect_and_focus( self, session: "VoiceTranslationSession", guild_id: int, channel_id: int, target: SpeakerIdentity, ): if not self.is_ready: raise RuntimeError(f"{self.label} не готов") if self.voice_client and self.voice_client.is_connected(): await self.disconnect(flush=False) guild = self.client.get_guild(guild_id) if guild is None: raise RuntimeError( f"Worker #{self.number} не видит guild {guild_id}. " "Проверь, что worker-бот добавлен на сервер." ) channel = guild.get_channel(channel_id) if not isinstance(channel, (discord.VoiceChannel, discord.StageChannel)): raise RuntimeError( f"Worker #{self.number} не видит voice channel {channel_id}" ) self.session = session self.target = target self.target_user_id = target.user_id self.audio = AudioBuffer() print( f"[ASSIGN] {self.label} -> " f"{target.display_name} ({target.user_id})" ) try: self.voice_client = await channel.connect( cls=voice_recv.VoiceRecvClient, self_deaf=False, self_mute=True, reconnect=True, ) self.sink = FocusedSink(self) self.running = True self.voice_client.listen( self.sink, after=self._listen_finished, ) self.flusher_task = asyncio.create_task( self.flush_loop(), name=f"voice-worker-{self.number}-flusher", ) except Exception: self.running = False self.sink = None self.target = None self.target_user_id = None self.session = None self.audio = AudioBuffer() if self.voice_client: try: await self.voice_client.disconnect(force=True) except Exception: pass self.voice_client = None raise def _listen_finished(self, error: Optional[Exception]): if error: print( f"[WORKER VOICE ERROR] #{self.number}: " f"{type(error).__name__}: {error}" ) def add_audio(self, pcm: bytes): if not self.running or self.target_user_id is None: return wall_now = datetime.now().astimezone() mono_now = time.monotonic() with self.audio_lock: if not self.audio.pcm: # This is the timestamp that goes into the user's text log. # It is captured when the first PCM bytes of the phrase arrive, # not when ASR/translation later finishes. self.audio.heard_at = wall_now self.audio.pcm.extend(pcm) self.audio.last_packet_monotonic = mono_now self.audio.ended_at = wall_now def pop_ready_audio(self, force: bool = False) -> Optional[CapturedPhrase]: with self.audio_lock: pcm_size = len(self.audio.pcm) if pcm_size == 0: return None silent_long_enough = ( time.monotonic() - self.audio.last_packet_monotonic >= SILENCE_SECONDS ) max_length_reached = pcm_size >= MAX_AUDIO_BYTES if not (force or silent_long_enough or max_length_reached): return None pcm = bytes(self.audio.pcm) heard_at = self.audio.heard_at or datetime.now().astimezone() ended_at = self.audio.ended_at or heard_at self.audio = AudioBuffer() audio_seconds = len(pcm) / BYTES_PER_SECOND if len(pcm) < MIN_AUDIO_BYTES: print( f"[AUDIO SKIP] worker={self.number} | " f"{audio_seconds:.2f}s слишком коротко" ) return None return CapturedPhrase( pcm=pcm, heard_at=heard_at, ended_at=ended_at, ) async def flush_loop(self): try: while self.running: await asyncio.sleep(0.15) phrase = self.pop_ready_audio() if not phrase: continue session = self.session target = self.target if session and target: session.submit_segment( target, phrase, self.number, ) except asyncio.CancelledError: pass except Exception: print(f"[WORKER FLUSH ERROR] #{self.number}") traceback.print_exc() async def disconnect(self, flush: bool = True): old_session = self.session old_target = self.target self.running = False if self.voice_client: try: if self.voice_client.is_listening(): self.voice_client.stop_listening() except Exception: traceback.print_exc() if self.flusher_task: self.flusher_task.cancel() try: await self.flusher_task except asyncio.CancelledError: pass self.flusher_task = None if flush and old_session and old_target: phrase = self.pop_ready_audio(force=True) if phrase: old_session.submit_segment( old_target, phrase, self.number, ) if self.voice_client: try: await self.voice_client.disconnect(force=True) except Exception as exc: print(f"[WORKER DISCONNECT ERROR] #{self.number}: {exc}") self.voice_client = None self.sink = None self.target = None self.target_user_id = None self.session = None self.audio = AudioBuffer() print(f"[WORKER FREE] #{self.number}") async def close(self): try: await self.disconnect(flush=False) except Exception: pass try: await self.client.close() except Exception: pass
WORKERS: list[VoiceWorker] = []
ACTIVE_SESSION: Optional["VoiceTranslationSession"] = None
class VoiceTranslationSession:
def init(self, guild_id: int, voice_channel_id: int):
self.guild_id = guild_id
self.voice_channel_id = voice_channel_id
textself.running = False self.assignments: dict[int, VoiceWorker] = {} self.reconcile_lock = asyncio.Lock() self.ai_semaphore = asyncio.Semaphore(AI_PARALLELISM) self.processing_tasks: set[asyncio.Task] = set() self.last_capacity_warning_at = 0.0 def get_controller_channel(self): guild = bot.get_guild(self.guild_id) if guild is None: return None return guild.get_channel(self.voice_channel_id) def human_members(self) -> list[discord.Member]: channel = self.get_controller_channel() if not isinstance(channel, (discord.VoiceChannel, discord.StageChannel)): return [] return [member for member in channel.members if not member.bot] def ready_workers(self) -> list[VoiceWorker]: return [ worker for worker in WORKERS if ( worker.is_ready and worker.client.get_guild(self.guild_id) is not None ) ] async def start(self): humans = self.human_members() ready_workers = self.ready_workers() if len(humans) > len(ready_workers): raise RuntimeError( f"В voice сейчас {len(humans)} чел., " f"а готовых worker-ботов только {len(ready_workers)}. " f"Не хватает {len(humans) - len(ready_workers)}." ) self.running = True await self.reconcile() async def stop(self): self.running = False workers = list(self.assignments.values()) self.assignments.clear() await asyncio.gather( *(worker.disconnect(flush=True) for worker in workers), return_exceptions=True, ) current_tasks = list(self.processing_tasks) if current_tasks: await asyncio.gather( *current_tasks, return_exceptions=True, ) async def reconcile(self): if not self.running: return async with self.reconcile_lock: humans = self.human_members() current_ids = {member.id for member in humans} assigned_ids = set(self.assignments.keys()) left_ids = assigned_ids - current_ids for user_id in left_ids: worker = self.assignments.pop(user_id, None) if worker: print( f"[LEAVE] user={user_id} -> " f"free worker #{worker.number}" ) await worker.disconnect(flush=True) new_members = [ member for member in humans if member.id not in self.assignments ] free_workers = [ worker for worker in self.ready_workers() if not worker.is_busy ] for member in new_members: if not free_workers: now = time.monotonic() if now - self.last_capacity_warning_at >= 5.0: self.last_capacity_warning_at = now print( "[CAPACITY] Не хватает worker-ботов | " f"humans={len(humans)} | " f"ready={len(self.ready_workers())} | " f"unassigned={member.display_name} ({member.id})" ) continue worker = free_workers.pop(0) identity = SpeakerIdentity.from_member(member) try: await worker.connect_and_focus( self, self.guild_id, self.voice_channel_id, identity, ) except Exception as exc: print( f"[ASSIGN ERROR] worker={worker.number} " f"user={member.id}: {exc}" ) traceback.print_exc() await worker.disconnect(flush=False) continue self.assignments[member.id] = worker print( f"[ASSIGNED] worker=#{worker.number} | " f"user={member.display_name} ({member.id}) | " f"log={speaker_log_path(identity)}" ) def submit_segment( self, speaker: SpeakerIdentity, phrase: CapturedPhrase, worker_number: int, ): if not phrase.pcm: return task = asyncio.create_task( self.process_segment( speaker, phrase, worker_number, ), name=f"stt-{speaker.user_id}-{time.monotonic_ns()}", ) self.processing_tasks.add(task) task.add_done_callback(self.processing_tasks.discard) async def process_segment( self, speaker: SpeakerIdentity, phrase: CapturedPhrase, worker_number: int, ): async with self.ai_semaphore: try: audio_seconds = len(phrase.pcm) / BYTES_PER_SECOND print("\n" + "=" * 72) print( f"[AUDIO] worker={worker_number} | " f"{speaker.display_name} ({speaker.user_id}) | " f"{audio_seconds:.2f}s | " f"heard={format_heard_timestamp(phrase.heard_at)}" ) transcription = await asyncio.to_thread( transcribe_multilingual_pcm, phrase.pcm, ) if not transcription.text: print("[STT EMPTY / NOISE]") return russian_text = await asyncio.to_thread( translate_transcription_to_russian, transcription, ) if not russian_text: print("[TRANSLATE EMPTY]") return print(f"[STT FULL] {transcription.text}") print(f"[TRANSLATE] {russian_text}") log_path = await asyncio.to_thread( append_phrase_log, speaker, worker_number, phrase.heard_at, phrase.ended_at, transcription, russian_text, ) print(f"[FILE LOG] {log_path}") except Exception: print( f"[AI ERROR] user={speaker.user_id} " f"worker={worker_number}" ) traceback.print_exc() finally: print("=" * 72)
@bot.event
async def on_ready():
ready_workers = [worker for worker in WORKERS if worker.is_ready]
failed_workers = [worker for worker in WORKERS if worker.failed_reason]
textprint("=" * 72) print( f"Controller: {bot.user} | " f"ID={bot.user.id if bot.user else 'n/a'}" ) print(f"Workers configured: {len(WORKERS)}") print(f"Workers ready: {len(ready_workers)}") print(f"Workers failed: {len(failed_workers)}") print(f"General Whisper: {GENERAL_WHISPER_MODEL}") print( "Georgian fine-tune: " f"{GEORGIAN_WHISPER_MODEL if georgian_whisper else 'OFF'}" ) print(f"faster-whisper: {FASTER_WHISPER_VERSION}") print( f"Language detection segments: {LANGUAGE_DETECTION_SEGMENTS} " "(whole-phrase LID only; no ASR splitting)" ) print(f"VAD threshold: {VAD_THRESHOLD:.2f}") print(f"AI parallelism: {AI_PARALLELISM}") print(f"CPU thread budget: {CPU_THREAD_BUDGET}/{CPU_COUNT}") print(f"Local logs: {LOG_DIR}") print("Discord translation output channel: DISABLED") print("OpenAI API: НЕ ИСПОЛЬЗУЕТСЯ") print("=" * 72) for worker in failed_workers: print( f"[FAILED WORKER] #{worker.number}: " f"{worker.failed_reason}" )
@bot.event
async def on_voice_state_update(
member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState,
):
global ACTIVE_SESSION
textsession = ACTIVE_SESSION if session is None or not session.running: return if member.bot: return before_id = before.channel.id if before.channel else None after_id = after.channel.id if after.channel else None if session.voice_channel_id in {before_id, after_id}: await session.reconcile()
@bot.command(name="R")
@commands.guild_only()
async def voice_translate_command(
ctx: commands.Context,
action: str = "",
):
global ACTIVE_SESSION
textaction = action.lower().strip() if action == "vois": if not isinstance(ctx.author, discord.Member): return voice_state = ctx.author.voice if voice_state is None or voice_state.channel is None: await ctx.reply("❌ Сначала зайди в голосовой канал.") return if ACTIVE_SESSION and ACTIVE_SESSION.running: await ctx.reply( "⚠️ Уже запущена одна общая voice-сессия. " "Сначала используй `!R stop`." ) return ready_workers = [ worker for worker in WORKERS if ( worker.is_ready and worker.client.get_guild(ctx.guild.id) is not None ) ] human_count = len( [ member for member in voice_state.channel.members if not member.bot ] ) if human_count > len(ready_workers): await ctx.reply( "❌ **Не хватает ботов для перевода.**\n" f"Людей в voice: **{human_count}**\n" f"Готовых worker-ботов: **{len(ready_workers)}**\n" f"Не хватает: **{human_count - len(ready_workers)}**" ) return session = VoiceTranslationSession( guild_id=ctx.guild.id, voice_channel_id=voice_state.channel.id, ) ACTIVE_SESSION = session try: await session.start() except Exception as exc: ACTIVE_SESSION = None traceback.print_exc() await ctx.reply( "❌ Не удалось запустить перевод:\n" f"`{type(exc).__name__}: {exc}`" ) return await ctx.reply( f"🎙️ Перевод запущен в **{voice_state.channel.name}**.\n" f"👥 Людей: **{human_count}**.\n" f"🤖 Worker-ботов в работе: " f"**{len(session.assignments)}** / **{len(ready_workers)}**.\n" f"🎯 Каждый worker принимает PCM только от одного User ID.\n" f"🇬🇪 Georgian fine-tune проверяет каждую цельную фразу с `language=ka`.\n" f"🧠 Внутреннее ASR-разбиение на языковые VAD-окна отключено.\n" f"🔇 VAD используется только как speech/noise gate и обрезка краёв.\n" f"🇷🇺 Всё переводится на русский.\n" f"📁 Результаты пишутся локально в папку `logs/`, по отдельному TXT на человека.\n" f"🕒 Таймкод берётся в момент прихода первого PCM-пакета фразы.\n" f"🛑 Остановка: `!R stop`" ) return if action == "stop": session = ACTIVE_SESSION if session is None or not session.running: await ctx.reply("❌ Перевод сейчас не запущен.") return ACTIVE_SESSION = None await ctx.reply("⏳ Останавливаю voice-перевод...") await session.stop() await ctx.reply( "🛑 Перевод остановлен, worker-боты вышли из voice. " "Последние фразы дописаны в локальные TXT." ) return if action == "status": ready_workers = [worker for worker in WORKERS if worker.is_ready] busy_workers = [ worker for worker in ready_workers if worker.is_busy ] if ACTIVE_SESSION and ACTIVE_SESSION.running: await ctx.reply( f"✅ Перевод активен.\n" f"🤖 Workers: **{len(busy_workers)} занято / " f"{len(ready_workers)} готово**.\n" f"👥 Закреплено пользователей: " f"**{len(ACTIVE_SESSION.assignments)}**.\n" f"🧵 AI parallelism: **{AI_PARALLELISM}**.\n" f"🖥️ CPU thread budget: " f"**{CPU_THREAD_BUDGET}/{CPU_COUNT}**.\n" f"🇬🇪 Georgian candidate: **whole phrase / always tried**.\n" f"📁 Output: **local logs/**.\n" f"📦 faster-whisper: **{FASTER_WHISPER_VERSION}**." ) else: await ctx.reply( f"⏹️ Перевод не запущен.\n" f"🤖 Готовых worker-ботов: **{len(ready_workers)}**.\n" f"🧵 AI parallelism: **{AI_PARALLELISM}**.\n" f"🖥️ CPU thread budget: " f"**{CPU_THREAD_BUDGET}/{CPU_COUNT}**.\n" f"📁 Output: **local logs/**.\n" f"📦 faster-whisper: **{FASTER_WHISPER_VERSION}**." ) return await ctx.reply( "Команды:\n" "`!R vois` — начать перевод в твоём voice\n" "`!R stop` — остановить и вывести worker-ботов\n" "`!R status` — показать состояние" )
@voice_translate_command.error
async def voice_translate_error(
ctx: commands.Context,
error: Exception,
):
print(f"[COMMAND ERROR] {type(error).name}: {error}")
traceback.print_exception(
type(error),
error,
error.traceback,
)
texttry: await ctx.reply( "❌ Ошибка команды: " f"`{type(error).__name__}: {error}`" ) except discord.DiscordException: pass
async def main():
global WORKERS
global ACTIVE_SESSION
textWORKERS = [ VoiceWorker(number=i, token=token) for i, token in enumerate(WORKER_TOKENS, start=1) ] worker_tasks = [ asyncio.create_task( worker.run_client(), name=f"worker-client-{worker.number}", ) for worker in WORKERS ] startup_waiters = [ asyncio.wait_for(worker.ready_event.wait(), timeout=30.0) for worker in WORKERS ] await asyncio.gather( *startup_waiters, return_exceptions=True, ) ready_count = len([worker for worker in WORKERS if worker.is_ready]) print(f"[STARTUP] Ready workers: {ready_count}/{len(WORKERS)}") try: await bot.start(CONTROLLER_TOKEN, reconnect=True) finally: if ACTIVE_SESSION: try: await ACTIVE_SESSION.stop() except Exception: pass ACTIVE_SESSION = None await asyncio.gather( *(worker.close() for worker in WORKERS), return_exceptions=True, ) for task in worker_tasks: if not task.done(): task.cancel() await asyncio.gather( *worker_tasks, return_exceptions=True, )
if name == "main":
asyncio.run(main())
код
DISCORD_TOKEN=MTMyNDA0MDI4MzkzOTE0MzY5Mg.G0HhOx.CKmUfrMpt4DDh02xUmEpoPspekVMTh11t_5lO0
DISCORD_TOKEN1=MTM3ODMxNjk3NjI5MTU4MjAwMg.Goe5hO.g7qK2qM_oSYnMEBdCfOWYBh72K9fGdC0Tk4Vnw
DISCORD_TOKEN2=MTM3ODMyMDc2MjA0OTUyNzgzOQ.Grn7OR.uJA7fbc41AEuqGlb1ZWBB_05HD_-SaKUOn0xm0
DISCORD_TOKEN3=MTM3ODc5Nzg4MzI3MjIwMDMwNA.G6z1E4.ecwjcus9yEQLkSTaccuZbbvMO9HU63QTSax4OM
DISCORD_TOKEN4=MTM3ODc5ODk5NDA4MzU0OTIyNQ.G8BUEX.zFBDruVTuOLwz1YPaRA16i7poETKq-eFhstK9U
DISCORD_TOKEN5=MTM3ODc5OTI3Mzk5NDYxNjg3Mw.Gq2mMQ.ACx3p4DcEnc9VtjEBFGvYUhk9b4Eo138AXd0ho
DISCORD_TOKEN6=MTM3ODc5OTUxMzMyNTY2NjM1NA.GDDn74.FUM80StS_KznyNnkD1AcmqNDRmpMcP7eqAPyFc
DISCORD_TOKEN7=MTM3ODc5OTczMjA3MTMzODA5NA.Gq0LbX.rmXp5wQxOAUfZHumgb5k3E2du4kWN0780mrp8c
DISCORD_TOKEN8=MTM3ODc5OTk4Mjg1MjgzNzU5OA.GJp-dO.9Pwtmf-_1s6I1UUu0m3rW7_3S_80F9B8HyqfoQ
DISCORD_TOKEN9=MTM3ODgwMDE5NTY5Njg1MzE0NA.GcuHv5.5UP5X0LLvoFlWPYWADQa8GRHC5nbnuuiK1BaF0
DISCORD_TOKEN10=MTM3ODgwMDM4NDY1NDcwNDczMg.Gzp3JN.d5eA67qS9EDq2tv57E0VXObomOBWDy3BX29htQ
DISCORD_TOKEN11=MTM3ODgwMDY4NjEyMDE3NzcxNQ.Gss32B.dLoncwJfiysdHF_ObQmLJhrrwcKb-6XQVD-Ls
DISCORD_TOKEN12=MTM4NjEwNzMyMzk5NDYwNzY5Ng.GrWQO2.XRlkaD7LI4w0KQ-HQK69ubnPTfVqPAMFychgSA
DISCORD_TOKEN13=MTM4NjEwNzc2MDMzMjI0NzEyMA.GxtY0N.ig5AFQ1mBYHdjP9gWnIaGfUKRRpqI2KqxM7mFk
DISCORD_TOKEN14=MTM4NjEwODI0NjcwNzgwMjIzMw.GXCJiy.rSae0zn-Qu-XVD8VW8t3-GA2G8Nw99FrTj1exM
DISCORD_TOKEN15=MTM4NjEwODYzMzAyNDI5OTEwOA.GzzLqb.eoNyFge3VsyCqv0SSk06JjJ14XqGbC1_cFnsTY
DISCORD_TOKEN16=MTM4NjEwODkyMTUxMjQ2MDM0OA.GqBLoL.P1kMuze9LROzv3Zej7sOCqtt6XnsLfDqdRCbtw
DISCORD_TOKEN17=MTM4NjEwOTEwMTY3NDcyNTQyOA.GqcAFP.tUu6tKQuYflbvQZT_opDAG8YOj-JFRGqYC3zOE
DISCORD_TOKEN18=MTM4NjEwOTI5NTg3OTUyNDQ5Mg.GH4oGG.hJ2o2yQi3X7Kb_V2EWtnJud5PsUpzvRzvM1Db4
DISCORD_TOKEN19=MTM4NjEwOTUyNzAxNDg5OTgwMw.G0ORFq.A2loS_WsHfHN3ctS5Uq-XrA8yqfU42LITLJMwo
DISCORD_TOKEN20=MTM4NjEwOTY4MjA0ODgzMTYyMA.GIO0s.Hpp7qa5quHgKg_KtZpWcSgngBR8KcjsLuLTdg8
GENERAL_WHISPER_MODEL=large-v3
GEORGIAN_WHISPER_MODEL=LukeJacob2023/whisper-large-v3-turbo-ka-ct2-gguf
USE_GEORGIAN_FINE_TUNE=1
LANGUAGE_DETECTION_SEGMENTS=3
GEORGIAN_CANDIDATE_PROB=0.08
GEORGIAN_STRONG_PROB=0.16
GEORGIAN_MAX_PROB_GAP=0.50
GEORGIAN_QUALITY_MARGIN=0.42
GEORGIAN_CONFUSABLE_MIN_PROB=0.02
GEORGIAN_MIN_SCRIPT_RATIO=0.45
GEORGIAN_STRONG_WORD_PROB=0.58
GEORGIAN_STRONG_LOGPROB=-0.85
GEORGIAN_CONFUSABLE_LANGS=fr,he,ar,fa,hy,el,tr,az
NLLB_MODEL_NAME=facebook/nllb-200-distilled-600M
NLLB_MAX_INPUT_TOKENS=280
NLLB_MAX_NEW_TOKENS=420
SILENCE_SECONDS=0.75
MIN_AUDIO_SECONDS=0.30
MAX_AUDIO_SECONDS=12.0
VAD_THRESHOLD=0.58
VAD_MIN_SPEECH_MS=180
VAD_MIN_SILENCE_MS=180
VAD_SPEECH_PAD_MS=160
MIN_AUDIO_DBFS=-54.0
ASR_MIN_AVG_LOGPROB=-1.10
ASR_MAX_NO_SPEECH_PROB=0.76
ASR_MIN_WORD_PROB=0.30
ASR_LOW_LANGUAGE_PROB=0.18
ASR_LOW_LANG_MAX_CHARS=12
CPU_THREAD_BUDGET_PERCENT=65
AI_PARALLELISM=2
env
discord.py[voice]==2.7.1
git+https://github.com/rdphillips7/discord-ext-voice-recv.git
faster-whisper>=1.2.1
transformers>=4.46.0
torch
sentencepiece
protobuf
python-dotenv
pycountry
numpy>=1.26
тхт
бля пиздец хуйня сука он плохо есть куча проблем(надо имено бесплатно)
1 1 слово рускрое потом гнрузин идёт но он засчитывает руский и не переводит
(не делай кривую сегминтацию ведь он начинает непрпвельно вырезать и хуярить что это вообще арабский или еврейский тоесть хуйня сегментация)
2 он постояно слышет несуществующие шумы на англ или чтото ткое + он нечётко руский воспринимает
ведь у него русалка это рухалка
возмонжо стоит перейти на другой ии но тогда надо чтобы он был именно ебанутый но бесплатным и ты его с 1 попытки пиздато написал и гайд как скачать
НО ЕСЛЕ МОЖНО исправить все проблемы не меняя ии то лучше не менять
итого скинь весь новый готовый код
Да. Я не менял основной AI: остаются faster-whisper large-v3, твой Georgian fine-tune и локальный NLLB. Главный баг был в логике: после LID ты передавал language=detected_language, фактически заставляя всю фразу быть одним языком. В faster-whisper 1.2.1 есть multilingual=True для многоязычного аудио; condition_on_previous_text=False также важен для такого режима. (GitHub)
Плюс я сильно ограничил Georgian fallback. Твоя грузинская модель сама предупреждает, что на шуме, других языках и code-switching может выдавать неправильный текст — именно это видно в строках вроде случайного набора грузинских букв. (huggingface.co)
Готовый комплект: скачать ZIP со всем проектом
Отдельно: готовый 1.py и инструкция README.
Что изменено:
large-v3 теперь работает с language=None + multilingual=True, поэтому русское слово → грузинская речь больше не принудительно превращается целиком в русский.ka≈0 он вообще не имеет права победить. Это должно убрать бред типа ფერთვილო უყნდღმას из шума.�, , ჱ, повышенный short-audio gate, проверку VAD islands / ASR words, no_speech, word probability и подозрительно длинных псевдослов.ru и с beam 7. Для уверенного русского второй проход не тратится.WHISPER_HOTWORDS=русалка,майнкрафт,..., если конкретные слова постоянно коверкаются.[JOB N].py_compile — синтаксически готов.Установка простая: распакуй ZIP вместо старого проекта, выполни python -m pip install -r requirements.txt, скопируй .env.example в .env, вставь токены и запусти python 1.py. Модели по-прежнему скачиваются автоматически с Hugging Face и работают локально; платный OpenAI API не используется.
Очень важно: токены controller + всех 20 worker-ботов, которые ты вставил сюда, теперь надо считать скомпрометированными. Сбрось все 21 токен в Discord Developer Portal → соответствующее Application → Bot → Reset Token. В мои файлы я эти токены специально не переносил — там только placeholders.
И ещё: в старых логах часть ощущения, что «ответ относится вообще к другой фразе», возникала потому, что у тебя одновременно работают AI_PARALLELISM=2, а print() из двух потоков свободно перемешивался. В новой версии этот визуальный бардак тоже устранён.