This site is developed to XHTML and CSS2 W3C standards.
If you see this paragraph, your browser does not support those standards and you
need to upgrade. Visit WaSP
for a variety of options.
Paste #65
Posted by: dsalin-chat
Posted on: 2025-10-28 19:41:51
Age: 28 days ago
Views: 23
# chat_voice_client_with_upload.py
import socket
import threading
import time
import zlib
import base64
import numpy as np
import pyaudio
import sounddevice as sd
import opuslib
import requests
import queue
import os
import ttkbootstrap as tb
from ttkbootstrap.constants import *
from tkinter import simpledialog, messagebox, scrolledtext, filedialog, ttk
import tkinter as tk
# ---------------- Config (chat & voice) ----------------
AUDIO_RATE = 8000
AUDIO_CHANNELS = 1
AUDIO_FORMAT = pyaudio.paInt16
CHUNK = 320 # увеличенный размер пакета (40ms)
ZLIB_LEVEL = 9
VOICE_PREFIX = "!voice "
MAX_CHUNK_SIZE = 400 # base64 одного кусочка
SLEEP_BETWEEN_CHUNKS = 0.05 # 50ms
client_socket = None
stop_event = threading.Event()
ignore_list = []
# Voice (Opus + sounddevice)
VOICE_SERVER_IP = "hoho.ws"
VOICE_SERVER_PORT = 50007
RATE = 16000
CHANNELS = 1
FRAME_SIZE = 960 # Opus frame (approx 60ms at 16kHz)
QUEUE_MAXSIZE = 50
voice_sock = None
encoder = opuslib.Encoder(RATE, CHANNELS, application="voip")
decoder = opuslib.Decoder(RATE, CHANNELS)
audio_queue = queue.Queue(maxsize=QUEUE_MAXSIZE)
send_audio = False
sd_stream = None
# HTTP upload server (DXP-0002)
UPLOAD_SERVER_URL = "" # пример: "http://spburg.ddns.net:42439" — настраивается через меню
# ---------------- μ-law helpers (оставлены для совместимости, но в этой версии основного голоса мы используем Opus) ----------------
MU = 255
MAX = 32768
def linear2ulaw(pcm):
pcm = pcm.astype(np.int32)
sign = np.sign(pcm)
pcm_abs = np.abs(pcm)
magnitude = np.log1p(MU * pcm_abs / MAX) / np.log1p(MU)
ulaw = sign * magnitude
ulaw_uint = (((ulaw + 1) / 2) * 255).astype(np.uint8)
return ulaw_uint.tobytes()
def ulaw2linear(ulaw_bytes):
arr = np.frombuffer(ulaw_bytes, dtype=np.uint8).astype(np.float32)
f = (arr / 255.0) * 2.0 - 1.0
sign = np.sign(f)
mag = np.abs(f)
pcm = sign * ((np.expm1(np.log1p(MU) * mag)) * (MAX / MU))
pcm = np.clip(pcm, -32768, 32767).astype(np.int16)
return pcm.tobytes()
# ---------------- Network & chat ----------------
def connect_to_server():
global client_socket
ip = simpledialog.askstring("Подключение", "Введите IP сервера:")
port = simpledialog.askinteger("Подключение", "Введите порт:")
if not ip or not port:
return
try:
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((ip, port))
threading.Thread(target=receive_messages, daemon=True).start()
threading.Thread(target=send_keepalive, daemon=True).start()
chat_box.insert(tk.END, f"[+] Подключено к {ip}:{port}\n")
chat_box.see(tk.END)
except Exception as e:
messagebox.showerror("Ошибка подключения", str(e))
def receive_messages():
global client_socket
while not stop_event.is_set():
try:
data = client_socket.recv(4096)
if not data:
break
try:
message = data.decode('utf-8', errors='ignore').strip()
except Exception:
continue
if VOICE_PREFIX in message:
# мы удаляли старую !voice логику; если сервер всё ещё присылает такие пакеты,
# можно обработать их здесь, но в данной интеграции используем Opus TCP поток отдельно.
pass
if any(word in message for word in ignore_list):
continue
chat_box.insert(tk.END, message + "\n")
chat_box.see(tk.END)
except Exception:
break
def send_keepalive():
while not stop_event.is_set():
try:
if client_socket:
client_socket.send(b"/")
except:
break
time.sleep(5)
def send_message(event=None):
if not client_socket:
chat_box.insert(tk.END, "[!] Не подключено к серверу\n")
chat_box.see(tk.END)
return
msg = msg_entry.get()
if msg:
try:
client_socket.send(msg.encode('utf-8'))
msg_entry.delete(0, tk.END)
except Exception:
chat_box.insert(tk.END, "[!] Ошибка отправки\n")
chat_box.see(tk.END)
# ---------------- Menu actions (chat) ----------------
def menu_login():
user = simpledialog.askstring("Логин", "Введите логин:")
password = simpledialog.askstring("Логин", "Введите пароль:", show="*")
if user and password and client_socket:
client_socket.send(f"/login {user} {password}".encode('utf-8'))
def menu_register():
user = simpledialog.askstring("Регистрация", "Введите логин:")
password = simpledialog.askstring("Регистрация", "Введите пароль:", show="*")
if user and password and client_socket:
client_socket.send(f"/register {user} {password}".encode('utf-8'))
def menu_pm():
recipient = simpledialog.askstring("Приватное сообщение", "Кому отправить:")
text = simpledialog.askstring("Приватное сообщение", "Текст сообщения:")
if recipient and text and client_socket:
client_socket.send(f"/pm {recipient} {text}".encode('utf-8'))
def menu_join():
channel = simpledialog.askstring("Вход на канал", "Введите название канала:")
if channel and client_socket:
client_socket.send(f"/join_server {channel}".encode('utf-8'))
def menu_ignore():
global ignore_list
ignore_text = simpledialog.askstring("Игнор-лист", "Введите слова через запятую:")
if ignore_text:
ignore_list = [w.strip() for w in ignore_text.split(",") if w.strip()]
chat_box.insert(tk.END, f"[+] Игнор обновлён: {ignore_list}\n")
chat_box.see(tk.END)
def menu_theme(theme_name: str):
root.style.theme_use(theme_name)
# ---------------- Voice (Opus) functions ----------------
def audio_callback(indata, frames, time_info, status):
if send_audio:
try:
audio_queue.put_nowait(indata[:,0].copy())
except queue.Full:
pass
def audio_send_thread():
global voice_sock
while True:
try:
frame = audio_queue.get(timeout=0.1)
if len(frame) < FRAME_SIZE:
frame = np.pad(frame, (0, FRAME_SIZE - len(frame)), 'constant')
pcm16 = (frame * 32767).astype(np.int16)
packet = encoder.encode(pcm16.tobytes(), FRAME_SIZE)
voice_sock.sendall(len(packet).to_bytes(4, 'big') + packet)
except queue.Empty:
continue
except Exception as e:
print("Send error:", e)
break
def audio_receive_thread():
global voice_sock
try:
with sd.OutputStream(samplerate=RATE, channels=CHANNELS, blocksize=FRAME_SIZE) as out_stream:
buffer = b''
while True:
try:
data = voice_sock.recv(4096)
if not data:
break
buffer += data
while len(buffer) >= 4:
length = int.from_bytes(buffer[:4], 'big')
if len(buffer) < 4 + length:
break
packet = buffer[4:4+length]
buffer = buffer[4+length:]
try:
pcm = decoder.decode(packet, FRAME_SIZE)
audio = np.frombuffer(pcm, dtype=np.int16).astype(np.float32)/32768
out_stream.write(audio)
except opuslib.OpusError:
continue
except Exception as e:
print("Receive error:", e)
break
except Exception as e:
print("Output stream error:", e)
def switch_device(device_name):
global sd_stream
if sd_stream:
try:
sd_stream.stop(); sd_stream.close()
except:
pass
try:
sd_stream = sd.InputStream(
device=device_name,
channels=CHANNELS,
samplerate=RATE,
blocksize=FRAME_SIZE,
dtype='float32',
latency='low',
callback=audio_callback
)
sd_stream.start()
except Exception as e:
print(f"Cannot open microphone '{device_name}': {e}")
def toggle_send():
global send_audio
send_audio = var.get()
def select_device(event):
threading.Thread(
target=switch_device,
args=(input_devices[devices_combo.current()]["name"],),
daemon=True
).start()
def connect_voice_server():
global voice_sock
try:
voice_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
voice_sock.connect((VOICE_SERVER_IP, VOICE_SERVER_PORT))
threading.Thread(target=audio_send_thread, daemon=True).start()
threading.Thread(target=audio_receive_thread, daemon=True).start()
chat_box.insert(tk.END, f"[+] Подключено к голосовому серверу {VOICE_SERVER_IP}:{VOICE_SERVER_PORT}\n")
chat_box.see(tk.END)
except Exception as e:
messagebox.showerror("Голос", f"Ошибка подключения к голосовому серверу: {e}")
def set_voice_server():
global VOICE_SERVER_IP, VOICE_SERVER_PORT
ip = simpledialog.askstring("Голосовой сервер", "Введите IP сервера:", initialvalue=VOICE_SERVER_IP)
port = simpledialog.askinteger("Голосовой сервер", "Введите порт:", initialvalue=VOICE_SERVER_PORT)
if ip and port:
VOICE_SERVER_IP = ip
VOICE_SERVER_PORT = port
chat_box.insert(tk.END, f"[+] Голосовой сервер изменён на {ip}:{port}\n")
chat_box.see(tk.END)
# ---------------- HTTP File Upload (DXP-0002) ----------------
def set_upload_server():
global UPLOAD_SERVER_URL
new_url = simpledialog.askstring("HTTP Upload сервер",
"Введите базовый адрес (например http://example.com:42439):",
initialvalue=UPLOAD_SERVER_URL or "http://")
if new_url:
UPLOAD_SERVER_URL = new_url.rstrip('/')
chat_box.insert(tk.END, f"[+] Сервер загрузки изменён на {UPLOAD_SERVER_URL}\n")
chat_box.see(tk.END)
def upload_file():
if not UPLOAD_SERVER_URL:
messagebox.showwarning("Upload", "Сначала настройте адрес HTTP Upload сервера в меню.")
return
file_path = filedialog.askopenfilename(title="Выберите файл для отправки")
if not file_path:
return
chat_box.insert(tk.END, f"[⏫] Загружаю файл: {os.path.basename(file_path)}\n")
chat_box.see(tk.END)
def worker(path):
try:
with open(path, "rb") as f:
files = {"file": (os.path.basename(path), f)}
r = requests.post(f"{UPLOAD_SERVER_URL}/upload", files=files, timeout=60)
if r.status_code == 200:
try:
data = r.json()
url = data.get("url")
if url:
# Отправляем ссылку в чат как обычное сообщение
if client_socket:
try:
client_socket.send(url.encode('utf-8'))
except Exception:
pass
chat_box.insert(tk.END, f"[✅] Файл загружен: {url}\n")
else:
chat_box.insert(tk.END, f"[!] Сервер вернул ответ без url: {r.text[:200]}\n")
except Exception:
chat_box.insert(tk.END, f"[!] Не удалось разобрать JSON: HTTP {r.status_code}\n")
else:
chat_box.insert(tk.END, f"[!] Ошибка загрузки: HTTP {r.status_code}\n")
except Exception as e:
chat_box.insert(tk.END, f"[!] Ошибка загрузки: {e}\n")
chat_box.see(tk.END)
threading.Thread(target=worker, args=(file_path,), daemon=True).start()
# ---------------- GUI Setup ----------------
root = tb.Window(themename="cyborg")
root.title("Chat Client (Voice + Upload)")
root.geometry("920x700")
# Menus
menubar = tk.Menu(root)
root.config(menu=menubar)
connection_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Подключение", menu=connection_menu)
connection_menu.add_command(label="Подключиться к серверу", command=connect_to_server)
actions_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Действия", menu=actions_menu)
actions_menu.add_command(label="Логин", command=menu_login)
actions_menu.add_command(label="Регистрация", command=menu_register)
actions_menu.add_command(label="Приватное сообщение", command=menu_pm)
actions_menu.add_command(label="Войти на канал", command=menu_join)
actions_menu.add_command(label="Игнор-лист", command=menu_ignore)
theme_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Темы", menu=theme_menu)
for theme in ["cyborg","darkly","flatly","superhero","morph","solar"]:
theme_menu.add_command(label=theme, command=lambda t=theme: menu_theme(t))
voice_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Голосовой сервер", menu=voice_menu)
voice_menu.add_command(label="Настроить адрес", command=set_voice_server)
voice_menu.add_command(label="Подключиться", command=connect_voice_server)
upload_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="HTTP Upload сервер", menu=upload_menu)
upload_menu.add_command(label="Настроить адрес", command=set_upload_server)
# Chat box
chat_box = scrolledtext.ScrolledText(root, wrap="word", width=120, height=30,
bg="#1e1e1e", fg="white",
insertbackground="white", font=("Consolas", 11))
chat_box.pack(padx=10, pady=10, fill="both", expand=True)
# Message entry and buttons
frame_bottom = tb.Frame(root)
frame_bottom.pack(fill="x", padx=10, pady=5)
msg_entry = tb.Entry(frame_bottom, width=80)
msg_entry.pack(side="left", padx=5, fill="x", expand=True)
msg_entry.bind("<Return>", send_message)
send_btn = tb.Button(frame_bottom, text="Отправить", bootstyle=SUCCESS, command=send_message)
send_btn.pack(side="left", padx=5)
file_btn = tb.Button(frame_bottom, text="📎", bootstyle="secondary-outline", command=upload_file)
file_btn.pack(side="left", padx=5)
# Voice controls
voice_frame = tb.Frame(root)
voice_frame.pack(padx=10, pady=5, fill="x")
var = tk.BooleanVar()
ttk.Checkbutton(voice_frame, text="Говорить", variable=var, command=toggle_send).pack(side="left", padx=5)
ttk.Label(voice_frame, text="Выберите микрофон:").pack(side="left", padx=5)
devices = sd.query_devices()
input_devices = [d for d in devices if d["max_input_channels"] > 0]
device_names = [d["name"] for d in input_devices] if input_devices else []
devices_combo = ttk.Combobox(voice_frame, values=device_names, width=50)
if device_names:
devices_combo.current(0)
devices_combo.bind("<<ComboboxSelected>>", select_device)
devices_combo.pack(side="left", padx=5)
# Select first device automatically
if input_devices:
threading.Thread(target=switch_device, args=(input_devices[0]["name"],), daemon=True).start()
# ---------------- Closing ----------------
def on_closing():
global send_audio, stop_event
send_audio = False
stop_event.set()
try:
if client_socket:
client_socket.close()
except: pass
try:
if voice_sock:
voice_sock.close()
except: pass
try:
if sd_stream:
sd_stream.stop(); sd_stream.close()
except: pass
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
Download raw |
Create new paste