Microsoft Windows [Version 10.0.26200.9168] (c) Ко...

تم الإنشاء في: ٧ سبتمبر ٢٠٢٦

تمت الإجابة باستخدام 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

C:\Users\mamo5\discord_pr\slusht>python 1.py

Загрузка локальных AI-моделей...

[VERSIONS] faster-whisper=1.2.1 | transformers=5.16.1 | discord.py=2.7.1 | torch=2.14.0+cpu
[AI] CUDA не найдена, использую CPU
[CPU] cores=16 | thread budget=10/16 (~65%) | whisper_threads/job=5 | torch_threads=10
[AI] General Whisper: large-v3 | device=cpu | compute=int8 | workers=2
2026-09-07 22:06:15,785 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/Systran/faster-whisper-large-v3/revision/main "HTTP/1.1 200 OK"
[AI] Georgian fine-tune: LukeJacob2023/whisper-large-v3-turbo-ka-ct2-gguf
2026-09-07 22:06:21,515 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/LukeJacob2023/whisper-large-v3-turbo-ka-ct2-gguf/revision/main "HTTP/1.1 200 OK"
[AI] NLLB: facebook/nllb-200-distilled-600M | device=cpu
2026-09-07 22:06:24,541 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/facebook/nllb-200-distilled-600M/resolve/main/config.json "HTTP/1.1 307 Temporary Redirect"
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
2026-09-07 22:06:24,541 | WARNING | huggingface_hub.utils._http | Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
2026-09-07 22:06:24,575 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/facebook/nllb-200-distilled-600M/f8d333a098d19b4fd9a8b18f94170487ad3f821d/config.json?%2Ffacebook%2Fnllb-200-distilled-600M%2Fresolve%2Fmain%2Fconfig.json=&etag=%2291ba54326eccfdb17a06a7d110df3d71f39b84b8%22 "HTTP/1.1 200 OK"
2026-09-07 22:06:24,730 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/facebook/nllb-200-distilled-600M/resolve/main/tokenizer_config.json "HTTP/1.1 307 Temporary Redirect"
2026-09-07 22:06:24,764 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/facebook/nllb-200-distilled-600M/f8d333a098d19b4fd9a8b18f94170487ad3f821d/tokenizer_config.json?%2Ffacebook%2Fnllb-200-distilled-600M%2Fresolve%2Fmain%2Ftokenizer_config.json=&etag=%22742aa330eae62f661fe28ff3fdd1d216d1481294%22 "HTTP/1.1 200 OK"
2026-09-07 22:06:24,911 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/facebook/nllb-200-distilled-600M/resolve/main/tokenizer_config.json "HTTP/1.1 307 Temporary Redirect"
2026-09-07 22:06:24,946 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/facebook/nllb-200-distilled-600M/f8d333a098d19b4fd9a8b18f94170487ad3f821d/tokenizer_config.json?%2Ffacebook%2Fnllb-200-distilled-600M%2Fresolve%2Fmain%2Ftokenizer_config.json=&etag=%22742aa330eae62f661fe28ff3fdd1d216d1481294%22 "HTTP/1.1 200 OK"
2026-09-07 22:06:25,085 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/facebook/nllb-200-distilled-600M/tree/main/additional_chat_templates?recursive=false&expand=false "HTTP/1.1 404 Not Found"
2026-09-07 22:06:25,227 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/facebook/nllb-200-distilled-600M/tree/main?recursive=true&expand=false "HTTP/1.1 200 OK"
2026-09-07 22:06:26,586 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/facebook/nllb-200-distilled-600M "HTTP/1.1 200 OK"
2026-09-07 22:06:26,736 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/facebook/nllb-200-distilled-600M/resolve/main/config.json "HTTP/1.1 307 Temporary Redirect"
2026-09-07 22:06:26,772 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/facebook/nllb-200-distilled-600M/f8d333a098d19b4fd9a8b18f94170487ad3f821d/config.json?%2Ffacebook%2Fnllb-200-distilled-600M%2Fresolve%2Fmain%2Fconfig.json=&etag=%2291ba54326eccfdb17a06a7d110df3d71f39b84b8%22 "HTTP/1.1 200 OK"
2026-09-07 22:06:27,621 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/facebook/nllb-200-distilled-600M/resolve/main/model.safetensors "HTTP/1.1 404 Not Found"
2026-09-07 22:06:27,759 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/facebook/nllb-200-distilled-600M "HTTP/1.1 200 OK"
Loading weights: 100%|████████████████████████████████████████████████████████████| 512/512 [00:00<00:00, 19290.05it/s]
2026-09-07 22:06:27,906 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/facebook/nllb-200-distilled-600M/commits/main "HTTP/1.1 200 OK"
2026-09-07 22:06:28,077 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/facebook/nllb-200-distilled-600M/discussions?p=0 "HTTP/1.1 200 OK"
2026-09-07 22:06:28,217 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/facebook/nllb-200-distilled-600M/resolve/main/generation_config.json "HTTP/1.1 307 Temporary Redirect"
2026-09-07 22:06:28,233 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/models/facebook/nllb-200-distilled-600M/commits/refs%2Fpr%2F45 "HTTP/1.1 200 OK"
2026-09-07 22:06:28,253 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/facebook/nllb-200-distilled-600M/f8d333a098d19b4fd9a8b18f94170487ad3f821d/generation_config.json?%2Ffacebook%2Fnllb-200-distilled-600M%2Fresolve%2Fmain%2Fgeneration_config.json=&etag=%224c6d168201607a22255bee55a6ed092b0e3ed7a1%22 "HTTP/1.1 200 OK"
2026-09-07 22:06:28,469 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/facebook/nllb-200-distilled-600M/resolve/refs%2Fpr%2F45/model.safetensors.index.json "HTTP/1.1 404 Not Found"
[LOG] Local phrase logs: C:\Users\mamo5\discord_pr\slusht\logs
[AI] Все модели загружены

2026-09-07 22:06:28,582 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,583 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,584 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,584 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,595 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,596 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,596 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,596 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,596 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,596 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,597 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,597 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,598 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,598 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,598 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,598 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,599 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,599 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,599 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,600 | INFO | discord.client | logging in using static token
2026-09-07 22:06:28,713 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/facebook/nllb-200-distilled-600M/resolve/refs%2Fpr%2F45/model.safetensors "HTTP/1.1 302 Found"
2026-09-07 22:06:29,581 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 2cc8974ee096f02274a8cfb9c2b3ff38).
2026-09-07 22:06:29,609 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 3b55171666b29bbb934732f86ae49dc8).
2026-09-07 22:06:29,633 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 4ba1c6c958825beeb3c87e279f526eb2).
2026-09-07 22:06:29,650 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 0107f3aaaee48976c9eb2203105cf6b9).
2026-09-07 22:06:29,665 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 62a3eab1430649092cc2f9790f787aaf).
2026-09-07 22:06:29,668 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 793a4387949f68c0db3e9971540c77be).
2026-09-07 22:06:29,674 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: cfc53786910407e71b718ee316e7cf87).
2026-09-07 22:06:29,687 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: c4cebb119138a7de5172cada511554ea).
2026-09-07 22:06:29,696 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 09f50142fa85c619a6c0744948cf4d1b).
2026-09-07 22:06:29,698 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: dad8adc6cc3633bfe470cdd97243e2e5).
2026-09-07 22:06:29,707 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: c0c0950ed2d9d6080f29edaa79d921f5).
2026-09-07 22:06:29,753 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 095d6a16b0a321d5c2418cd7e981013b).
2026-09-07 22:06:29,771 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 51314e555c4e8399fdf7b7ca5f2741a3).
2026-09-07 22:06:29,840 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 80b559c72b77f6976d8b55d1101a4293).
2026-09-07 22:06:29,861 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 6967fb081620a33d8b5034d459300353).
2026-09-07 22:06:29,882 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: b23393d55f40b84dff06e0040f2ac667).
2026-09-07 22:06:29,928 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 5638d6d549c11ead392bc8d49995d401).
2026-09-07 22:06:30,016 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 1e3263794907b5f5b32cdbb2dd1710aa).
2026-09-07 22:06:30,068 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 186ff9555eed8fe3cb90b1c5c80251b4).
2026-09-07 22:06:30,213 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 2502e7cc4de9e80d535cf995e364ac40).
[WORKER READY] #17: eblan16#5725
[WORKER READY] #20: eblan19#7963
[WORKER READY] #1: eblan0#0390
[WORKER READY] #3: eblan2#3241
[WORKER READY] #14: eblan13#2920
[WORKER READY] #6: eblan5#5474
[WORKER READY] #11: eblan10#1099
[WORKER READY] #5: eblan4#4591
[WORKER READY] #15: eblan14#7115
[WORKER READY] #16: eblan15#7467
[WORKER READY] #10: eblan9#3496
[WORKER READY] #2: eblan1#4592
[WORKER READY] #13: eblan12#7851
[WORKER READY] #7: eblan6#8355
[WORKER READY] #4: eblan3#8614
[WORKER READY] #19: eblan18#4420
[WORKER READY] #12: eblan11#3393
[WORKER READY] #18: eblan17#1230
[WORKER READY] #8: eblan7#0199
[WORKER READY] #9: eblan8#5388
[STARTUP] Ready workers: 20/20
2026-09-07 22:06:32,241 | INFO | discord.client | logging in using static token
2026-09-07 22:06:33,328 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: 2e0d35a3e7e37a8e6e83137392326323).

Controller: RedMouse229#1522 | ID=1324040283939143692
Workers configured: 20
Workers ready: 20
Workers failed: 0
General Whisper: large-v3
Georgian fine-tune: LukeJacob2023/whisper-large-v3-turbo-ka-ct2-gguf
faster-whisper: 1.2.1
Language detection segments: 3 (whole-phrase LID only; no ASR splitting)
VAD threshold: 0.58
AI parallelism: 2
CPU thread budget: 10/16
Local logs: C:\Users\mamo5\discord_pr\slusht\logs
Discord translation output channel: DISABLED
OpenAI API: НЕ ИСПОЛЬЗУЕТСЯ

[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

========================================================================
[AUDIO] worker=2 | KOT_XLEB N (1359561978661703760) | 1.38s | heard=2026-09-07 22:06:52.022 +03:00
[AUDIO LEVEL] -27.9 dBFS
[VAD WHOLE] islands=1 | keeping one phrase 0.00-0.77s | 0.77s
[LANG WHOLE] top=ru:0.856 | ka=0.000 | ru:0.86, uk:0.02, pl:0.02, en:0.02, ja:0.01, fr:0.01, nn:0.01, es:0.01
2026-09-07 22:07:04,209 | INFO | faster_whisper | Processing audio with duration 00:03.552
[LANG WHOLE] top=ru:0.380 | ka=0.000 | ru:0.38, id:0.19, es:0.08, pl:0.08, en:0.07, pt:0.05, ja:0.02, fr:0.02
2026-09-07 22:07:07,735 | INFO | faster_whisper | Processing audio with duration 00:00.768
[KA CANDIDATE] valid=True | q=-0.041 | logp=-0.420 | nospeech=0.000 | wordp=0.541 | script=1.00 | 'სიკეროპა ს კ ი რ ო პ ე ცვლუთხნდღმწყზფშტბჭჯგაც მ აქჩა.ძჟ'
2026-09-07 22:07:43,859 | INFO | faster_whisper | Processing audio with duration 00:03.552
[KA CANDIDATE] valid=True | q=-0.071 | logp=-0.432 | nospeech=0.000 | wordp=0.516 | script=1.00 | 'პაბა-ფა ბ აქვეყნილოს წრმუშტკზთხდღგჭჯაც ა ქა.წ.ა'
2026-09-07 22:07:51,471 | INFO | faster_whisper | Processing audio with duration 00:00.768
[GENERAL CANDIDATE] lang=ru:0.856 | valid=True | q=0.035 | logp=-0.364 | nospeech=0.152 | wordp=0.711 | 'Сиськи-жопа, сиськи жопа. Сиський жопой, сиский жобой.'
[STT WHOLE] lang=ru:0.856 | model=large-v3-general | decision=general-wins | logp=-0.364 | nospeech=0.152 | wordp=0.711 | Сиськи-жопа, сиськи жопа. Сиський жопой, сиский жобой.
[STT FULL] Сиськи-жопа, сиськи жопа. Сиський жопой, сиский жобой.
[TRANSLATE] Сиськи-жопа, сиськи жопа. Сиський жопой, сиский жобой.
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\RedMouse_1105531792473198632.txt

========================================================================
[AUDIO] worker=1 | RedMouse (1105531792473198632) | 1.48s | heard=2026-09-07 22:06:55.143 +03:00
[AUDIO LEVEL] -32.5 dBFS
[VAD WHOLE] islands=1 | keeping one phrase 0.00-1.48s | 1.48s
[GENERAL CANDIDATE] lang=ru:0.380 | valid=True | q=0.142 | logp=-0.273 | nospeech=0.377 | wordp=0.942 | 'Па-па-па.'
[STT WHOLE] lang=ru:0.380 | model=large-v3-general | decision=general-wins | logp=-0.273 | nospeech=0.377 | wordp=0.942 | Па-па-па.
[STT FULL] Па-па-па.
[TRANSLATE] Па-па-па.
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\KOT_XLEB N_1359561978661703760.txt

========================================================================
[AUDIO] worker=1 | RedMouse (1105531792473198632) | 5.28s | heard=2026-09-07 22:06:58.064 +03:00
[AUDIO LEVEL] -27.4 dBFS
[VAD WHOLE] islands=5 | keeping one phrase 0.00-5.22s | 5.22s
[LANG WHOLE] top=es:0.550 | ka=0.000 | es:0.55, pt:0.27, en:0.04, it:0.04, ja:0.01, id:0.01, fr:0.01, ru:0.01
2026-09-07 22:08:17,777 | INFO | faster_whisper | Processing audio with duration 00:01.480
[LANG WHOLE] top=ru:0.981 | ka=0.000 | ru:0.98, en:0.01, uk:0.00, fr:0.00, la:0.00, es:0.00, ja:0.00, pl:0.00
2026-09-07 22:08:20,048 | INFO | faster_whisper | Processing audio with duration 00:05.216
[KA CANDIDATE] valid=True | q=-0.112 | logp=-0.453 | nospeech=0.000 | wordp=0.487 | script=1.00 | 'მართველობის აქტუნდგზღწყფხჭშკპცჯაქა დ ა ქა-ჩა.ძა'
2026-09-07 22:09:12,293 | INFO | faster_whisper | Processing audio with duration 00:01.480
[GENERAL CANDIDATE] lang=es:0.550 | valid=True | q=-0.357 | logp=-0.584 | nospeech=0.294 | wordp=0.598 | 'Menos.'
[STT WHOLE] lang=es:0.550 | model=large-v3-general | decision=general-wins | logp=-0.584 | nospeech=0.294 | wordp=0.598 | Menos.
[transformers] Both 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)
[STT FULL] Menos.
[TRANSLATE] По меньшей мере.
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\RedMouse_1105531792473198632.txt

========================================================================
[AUDIO] worker=2 | KOT_XLEB N (1359561978661703760) | 1.60s | heard=2026-09-07 22:07:05.427 +03:00
[AUDIO LEVEL] -24.3 dBFS
[VAD WHOLE] islands=1 | keeping one phrase 0.00-1.47s | 1.47s
[KA CANDIDATE] valid=True | q=-0.158 | logp=-0.490 | nospeech=0.000 | wordp=0.475 | script=1.01 | 'უბიქ, ტროვდეგ, ალღმყნფთხზშკს პჟაქტ-პ რ კ ბ ო გჰჭჯცწძქაჩ\u10cfა დ ა ქა. მართველობის აქტუნდგზღფხშკცჯყაქპჟწჭა დ ა ქა-ჩა.ძა'
2026-09-07 22:09:34,511 | INFO | faster_whisper | Processing audio with duration 00:05.216
[LANG WHOLE] top=tr:0.454 | ka=0.000 | tr:0.45, ru:0.17, ja:0.12, pl:0.07, ko:0.03, cs:0.03, hu:0.02, uk:0.02
2026-09-07 22:09:44,203 | INFO | faster_whisper | Processing audio with duration 00:01.472
[GENERAL CANDIDATE] lang=ru:0.981 | valid=True | q=0.141 | logp=-0.343 | nospeech=0.043 | wordp=0.732 | 'Минус на майнкрафте кубик, трубик, клубик. Клубок.'
[STT WHOLE] lang=ru:0.981 | model=large-v3-general | decision=general-wins | logp=-0.343 | nospeech=0.043 | wordp=0.732 | Минус на майнкрафте кубик, трубик, клубик. Клубок.
[STT FULL] Минус на майнкрафте кубик, трубик, клубик. Клубок.
[TRANSLATE] Минус на майнкрафте кубик, трубик, клубик. Клубок.
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\RedMouse_1105531792473198632.txt

========================================================================
[AUDIO] worker=1 | RedMouse (1105531792473198632) | 3.34s | heard=2026-09-07 22:07:07.482 +03:00
[AUDIO LEVEL] -25.0 dBFS
[VAD WHOLE] islands=2 | keeping one phrase 0.35-3.07s | 2.72s
[LANG WHOLE] top=ru:0.722 | ka=0.000 | ru:0.72, pl:0.08, uk:0.03, en:0.02, ro:0.02, lt:0.02, nn:0.01, cs:0.01
2026-09-07 22:10:06,802 | INFO | faster_whisper | Processing audio with duration 00:02.720
[KA CANDIDATE] valid=True | q=0.091 | logp=-0.334 | nospeech=0.000 | wordp=0.607 | script=1.01 | 'გალა ბოქვა, ესიცე, ქა-ქ, აქ ს და მაგიდენობრულშვს აქტყთხზფჯკცპჟღწძჭე�ჩქაჰ\u10cfა მ სა-ჱა. ი. ა.'
2026-09-07 22:10:35,027 | INFO | faster_whisper | Processing audio with duration 00:01.472
[GENERAL CANDIDATE] lang=tr:0.454 | valid=True | q=0.176 | logp=-0.317 | nospeech=0.074 | wordp=0.773 | 'Kalabalık paylaşılsak istedim.'
[STT WHOLE] lang=ka:0.500 | model=georgian-finetune | decision=confusable-tr | logp=-0.334 | nospeech=0.000 | wordp=0.607 | გალა ბოქვა, ესიცე, ქა-ქ, აქ ს და მაგიდენობრულშვს აქტყთხზფჯკცპჟღწძჭე�ჩქაჰ჏ა მ სა-ჱა. ი. ა.
[transformers] Both 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)
[KA CANDIDATE] valid=True | q=0.145 | logp=-0.274 | nospeech=0.000 | wordp=0.599 | script=1.00 | 'რო ქალკი შეუნსპყვა გა, რ ქ ს ლ ომ? იგულებს ართვდნოფშტყმცხზღკპჟწჯაჭე�ჩაქჹა დ ა ქა-ჰძა.'
2026-09-07 22:10:57,802 | INFO | faster_whisper | Processing audio with duration 00:02.720
[STT FULL] გალა ბოქვა, ესიცე, ქა-ქ, აქ ს და მაგიდენობრულშვს აქტყთხზფჯკცპჟღწძჭე�ჩქაჰ჏ა მ სა-ჱა. ი. ა.
[TRANSLATE] Галло Боква, Исе, Ча-Чэ, здесь С и Магдинобл Джентльмен выскочили в М-ЗА.
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\KOT_XLEB N_1359561978661703760.txt

========================================================================
[AUDIO] worker=2 | KOT_XLEB N (1359561978661703760) | 1.32s | heard=2026-09-07 22:07:11.352 +03:00
[AUDIO LEVEL] -21.8 dBFS
[VAD WHOLE] islands=1 | keeping one phrase 0.00-1.22s | 1.22s
[GENERAL CANDIDATE] lang=ru:0.722 | valid=True | q=0.135 | logp=-0.348 | nospeech=0.119 | wordp=0.801 | 'Рухалка села на спагатчик, к слову.'
[STT WHOLE] lang=ru:0.722 | model=large-v3-general | decision=general-wins | logp=-0.348 | nospeech=0.119 | wordp=0.801 | Рухалка села на спагатчик, к слову.
[STT FULL] Рухалка села на спагатчик, к слову.
[TRANSLATE] Рухалка села на спагатчик, к слову.
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\RedMouse_1105531792473198632.txt

========================================================================
[AUDIO] worker=1 | RedMouse (1105531792473198632) | 3.50s | heard=2026-09-07 22:07:13.868 +03:00
[AUDIO LEVEL] -20.0 dBFS
[VAD WHOLE] islands=2 | keeping one phrase 0.00-3.36s | 3.36s
[LANG WHOLE] top=ru:0.917 | ka=0.000 | ru:0.92, uk:0.02, fr:0.01, pl:0.01, ro:0.01, en:0.00, zh:0.00, lt:0.00
2026-09-07 22:11:22,983 | INFO | faster_whisper | Processing audio with duration 00:01.216
[LANG WHOLE] top=ru:0.738 | ka=0.000 | ru:0.74, pl:0.10, uk:0.04, ro:0.01, nn:0.01, lt:0.01, la:0.01, bg:0.01
2026-09-07 22:11:29,201 | INFO | faster_whisper | Processing audio with duration 00:03.360
[KA CANDIDATE] valid=True | q=0.167 | logp=-0.247 | nospeech=0.000 | wordp=0.591 | script=1.02 | 'გავხეს ძია ნა-შმა.დ იგანებულშო მდღვრთხმ აქტყსფჯკზჍცპჟჭჩწძქა დ ა ქა-ჰ\u10cfაა.'
2026-09-07 22:11:57,814 | INFO | faster_whisper | Processing audio with duration 00:01.216
[KA CANDIDATE] valid=True | q=0.209 | logp=-0.294 | nospeech=0.000 | wordp=0.718 | script=1.00 | 'ციხა, შენ დრო მუტვმ ი ქწლთს რ ლ ად სღაქა კბგზშ'
2026-09-07 22:12:02,002 | INFO | faster_whisper | Processing audio with duration 00:03.360
[GENERAL CANDIDATE] lang=ru:0.917 | valid=True | q=-0.157 | logp=-0.535 | nospeech=0.039 | wordp=0.576 | 'Так, а как я сделаю наш парад?'
[STT WHOLE] lang=ru:0.917 | model=large-v3-general | decision=general-wins | logp=-0.535 | nospeech=0.039 | wordp=0.576 | Так, а как я сделаю наш парад?
[STT FULL] Так, а как я сделаю наш парад?
[TRANSLATE] Так, а как я сделаю наш парад?
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\KOT_XLEB N_1359561978661703760.txt

========================================================================
[AUDIO] worker=1 | RedMouse (1105531792473198632) | 1.68s | heard=2026-09-07 22:07:18.463 +03:00
[AUDIO LEVEL] -29.5 dBFS
[VAD WHOLE] islands=1 | keeping one phrase 0.19-1.60s | 1.41s
[GENERAL CANDIDATE] lang=ru:0.738 | valid=True | q=0.050 | logp=-0.475 | nospeech=0.062 | wordp=0.807 | 'Тихо, шендедамутумикцрулэладсрак.'
[STT WHOLE] lang=ru:0.738 | model=large-v3-general | decision=general-wins | logp=-0.475 | nospeech=0.062 | wordp=0.807 | Тихо, шендедамутумикцрулэладсрак.
[STT FULL] Тихо, шендедамутумикцрулэладсрак.
[TRANSLATE] Тихо, шендедамутумикцрулэладсрак.
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\RedMouse_1105531792473198632.txt

========================================================================
[AUDIO] worker=2 | KOT_XLEB N (1359561978661703760) | 2.17s | heard=2026-09-07 22:07:21.039 +03:00
[AUDIO LEVEL] -28.0 dBFS
[VAD WHOLE] islands=1 | keeping one phrase 0.48-1.54s | 1.06s
[LANG WHOLE] top=en:0.233 | ka=0.000 | en:0.23, ru:0.22, pt:0.08, de:0.05, es:0.04, tr:0.04, pl:0.04, it:0.03
2026-09-07 22:12:28,183 | INFO | faster_whisper | Processing audio with duration 00:01.408
[LANG WHOLE] top=ru:0.343 | ka=0.000 | ru:0.34, es:0.11, en:0.10, pt:0.06, pl:0.05, fr:0.04, zh:0.03, it:0.03
2026-09-07 22:12:34,201 | INFO | faster_whisper | Processing audio with duration 00:01.056
[KA CANDIDATE] valid=True | q=0.192 | logp=-0.342 | nospeech=0.000 | wordp=0.762 | script=1.00 | 'ფერთვილო უყნდღმას.'
2026-09-07 22:12:46,854 | INFO | faster_whisper | Processing audio with duration 00:01.408
[GENERAL CANDIDATE] lang=en:0.233 | valid=False | q=-1.236 | logp=-1.231 | nospeech=0.457 | wordp=0.417 | 'testable nissan'
[STT WHOLE] lang=ka:0.500 | model=georgian-finetune | decision=general-invalid | logp=-0.342 | nospeech=0.000 | wordp=0.762 | ფერთვილო უყნდღმას.
[transformers] Both 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)
[STT FULL] ფერთვილო უყნდღმას.
[TRANSLATE] Цветовый орех.
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\RedMouse_1105531792473198632.txt

========================================================================
[AUDIO] worker=2 | KOT_XLEB N (1359561978661703760) | 2.36s | heard=2026-09-07 22:07:24.669 +03:00
[AUDIO LEVEL] -20.6 dBFS
[VAD] speech not found
[STT EMPTY / NOISE]

========================================================================
[AUDIO] worker=1 | RedMouse (1105531792473198632) | 6.82s | heard=2026-09-07 22:07:21.523 +03:00
[AUDIO LEVEL] -20.2 dBFS
[VAD WHOLE] islands=2 | keeping one phrase 0.00-6.82s | 6.82s
[KA CANDIDATE] valid=True | q=-0.153 | logp=-0.500 | nospeech=0.000 | wordp=0.496 | script=1.02 | 'იგერალობს კვუდნთხმცშტყფზღკ-პჟწძქჭ\u10cfჯაჰჩა დ აქა-ა, რ ა ქჹა. ი. ა.'
2026-09-07 22:13:12,960 | INFO | faster_whisper | Processing audio with duration 00:01.056
[LANG WHOLE] top=ru:0.975 | ka=0.000 | ru:0.97, en:0.01, uk:0.01, pl:0.00, nn:0.00, ja:0.00, km:0.00, be:0.00
2026-09-07 22:13:22,864 | INFO | faster_whisper | Processing audio with duration 00:06.820
[GENERAL CANDIDATE] lang=ru:0.343 | valid=True | q=-0.980 | logp=-0.822 | nospeech=0.651 | wordp=0.378 | 'Я думаю, мы будем...'
[STT WHOLE] lang=ru:0.343 | model=large-v3-general | decision=general-wins | logp=-0.822 | nospeech=0.651 | wordp=0.378 | Я думаю, мы будем...
[STT FULL] Я думаю, мы будем...
[TRANSLATE] Я думаю, мы будем...
[FILE LOG] C:\Users\mamo5\discord_pr\slusht\logs\KOT_XLEB N_1359561978661703760.txt

========================================================================
[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

============================================================

HELPERS / VERSION COMPATIBILITY

============================================================

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)

text
parameters = 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()

text
parameters = 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 )

============================================================

CONFIG

============================================================

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}"))
]

Local logs are written next to this Python file by default.

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)

Outer Discord phrase splitting. This is the only phrase segmentation left.

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"))

Discord decoded PCM: 48 kHz, stereo, signed 16-bit.

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)

faster-whisper / Silero VAD work at 16 kHz.

ASR_SAMPLE_RATE = 16_000

ASR models.

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"}

Whole-phrase language detection. This no longer creates ASR segments.

LANGUAGE_DETECTION_SEGMENTS = max(
1,
int(os.getenv("LANGUAGE_DETECTION_SEGMENTS", "3")),
)

Georgian routing is intentionally more aggressive than before because the

dedicated model is now always evaluated on the whole phrase.

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")
)

Languages which Whisper sometimes confuses with Georgian acoustically on

short/noisy Discord speech. Georgian candidate gets extra preference here.

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 / noise filtering. VAD is now ONLY a speech gate + edge crop.

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"))

Post-ASR gates.

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"))

Translation model.

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

============================================================

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 / GPU BUDGET

============================================================

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 COMPATIBILITY

============================================================

IMPORTANT: no max_speech_duration_s here. The old code used the language

window length as a VAD maximum speech length, which encouraged splitting.

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))

============================================================

AI LOAD

============================================================

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 HELPERS

============================================================

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]

text
lookup_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"),
}

text
scores = { 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

text
code = 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]

============================================================

AUDIO / ASR DATA TYPES

============================================================

@dataclass(frozen=True)
class LanguageDetection:
language: str
probability: float
all_probs: tuple[tuple[str, float], ...]

text
def 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()

text
with 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)

text
if 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

text
rms = 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.

text
We 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,
}

text
filtered = 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()
]

text
if 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,
}

text
filtered = 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.

text
Return 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)

text
if 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,))

============================================================

MIXED-TEXT TRANSLATION

============================================================

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"

text
if 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)

text
if 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 []

text
if _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()

text
if 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()

text
if 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

text
inputs = 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()

text
if 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] = []

text
for 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()

============================================================

LOCAL PHRASE LOGGING

============================================================

@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)

text
language_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 BOT

============================================================

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."""

text
def __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

text
intents = 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

text
self.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)

============================================================

CONTROLLER EVENTS

============================================================

@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]

text
print("=" * 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

text
session = 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()

============================================================

COMMANDS

============================================================

@bot.command(name="R")
@commands.guild_only()
async def voice_translate_command(
ctx: commands.Context,
action: str = "",
):
global ACTIVE_SESSION

text
action = 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,
)

text
try: await ctx.reply( "❌ Ошибка команды: " f"`{type(error).__name__}: {error}`" ) except discord.DiscordException: pass

============================================================

START ALL BOTS

============================================================

async def main():
global WORKERS
global ACTIVE_SESSION

text
WORKERS = [ 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())
код

SECURITY: generate NEW tokens in Discord Developer Portal.

Do not reuse tokens that were pasted into a chat or published anywhere.

Controller bot

DISCORD_TOKEN=MTMyNDA0MDI4MzkzOTE0MzY5Mg.G0HhOx.CKmUfrMpt4DDh02xUmEpoPspekVMTh11t_5lO0

Voice workers. Add up to 20.

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

... up to DISCORD_TOKEN20

Local log folder. Default is ./logs next to the Python file.

LOG_DIR=./logs

ASR

GENERAL_WHISPER_MODEL=large-v3
GEORGIAN_WHISPER_MODEL=LukeJacob2023/whisper-large-v3-turbo-ka-ct2-gguf
USE_GEORGIAN_FINE_TUNE=1

Whole-phrase language ID. No 2.8 second ASR language windows anymore.

LANGUAGE_DETECTION_SEGMENTS=3

Aggressive Georgian routing.

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

Translation

NLLB_MODEL_NAME=facebook/nllb-200-distilled-600M
NLLB_MAX_INPUT_TOKENS=280
NLLB_MAX_NEW_TOKENS=420

Discord phrase capture. This is the only phrase segmentation left.

SILENCE_SECONDS=0.75
MIN_AUDIO_SECONDS=0.30
MAX_AUDIO_SECONDS=12.0

Silero: speech gate / edge crop only, NOT ASR segmentation.

VAD_THRESHOLD=0.58
VAD_MIN_SPEECH_MS=180
VAD_MIN_SILENCE_MS=180
VAD_SPEECH_PAD_MS=160
MIN_AUDIO_DBFS=-54.0

Whisper post-filtering.

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 / concurrency

CPU_THREAD_BUDGET_PERCENT=65
AI_PARALLELISM=2

WHISPER_CPU_THREADS=4

TORCH_NUM_THREADS=8

WHISPER_COMPUTE_TYPE=float16

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 попытки пиздато написал и гайд как скачать

НО ЕСЛЕ МОЖНО исправить все проблемы не меняя ии то лучше не менять

итого скинь весь новый готовый код

الفكرة

Patched language detection and packaged the Discord voice translator

Worked for 5m 26s

الإجابة

Да. Я не менял основной 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.

Что изменено:

  1. large-v3 теперь работает с language=None + multilingual=True, поэтому русское слово → грузинская речь больше не принудительно превращается целиком в русский.
  2. Никаких старых фиксированных 2.8-секундных ASR-кусков. VAD только отсекает мусор и обрезает края. Дополнительный короткий LID-probe хвоста используется только для определения возможности RU→KA, а не для нарезки транскрипции.
  3. Если general Whisper уже написал одновременно кириллицу и грузинский — результат сохраняется, а NLLB переводит только грузинские токены.
  4. Для сложного случая «одно русское слово, потом грузинский» есть merge по word timestamps двух цельных декодов: русское начало сохраняется, грузинская часть берётся из Georgian expert.
  5. Georgian model больше не запускается на каждом аудио. При ka≈0 он вообще не имеет права победить. Это должно убрать бред типа ფერთვილო უყნდღმას из шума.
  6. Добавил проверку ненормальных грузинских символов вроде , , , повышенный short-audio gate, проверку VAD islands / ASR words, no_speech, word probability и подозрительно длинных псевдослов.
  7. Для неуверенного, но явно русского аудио возможен второй decode, уже принудительно ru и с beam 7. Для уверенного русского второй проход не тратится.
  8. Можно прописывать WHISPER_HOTWORDS=русалка,майнкрафт,..., если конкретные слова постоянно коверкаются.
  9. Логи двух параллельных пользователей теперь не перемешиваются: каждый результат печатается цельным блоком [JOB N].
  10. Код прогнан через 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() из двух потоков свободно перемешивался. В новой версии этот визуальный бардак тоже устранён.

مشاركة هذا السؤال والجواب