211 lines
6.8 KiB
Python
211 lines
6.8 KiB
Python
"""
|
||
Volume control module.
|
||
Regulates system volume on a scale from 1 to 10.
|
||
"""
|
||
|
||
# Модуль управления громкостью системы.
|
||
# Работает через различные системные утилиты в зависимости от ОС.
|
||
|
||
import subprocess
|
||
import re
|
||
import platform
|
||
from ..core.roman import replace_roman_numerals
|
||
|
||
try:
|
||
import pymorphy3
|
||
|
||
_MORPH = pymorphy3.MorphAnalyzer()
|
||
except Exception:
|
||
_MORPH = None
|
||
|
||
# Карта для перевода слов в цифры ("пять" -> 5)
|
||
NUMBER_MAP = {
|
||
"ноль": 0,
|
||
"один": 1,
|
||
"одна": 1,
|
||
"раз": 1,
|
||
"единица": 1,
|
||
"единичка": 1,
|
||
"два": 2,
|
||
"две": 2,
|
||
"двойка": 2,
|
||
"двоечка": 2,
|
||
"три": 3,
|
||
"тройка": 3,
|
||
"троечка": 3,
|
||
"четыре": 4,
|
||
"четверка": 4,
|
||
"четверочка": 4,
|
||
"пять": 5,
|
||
"пятерка": 5,
|
||
"пятерочка": 5,
|
||
"шесть": 6,
|
||
"шестерка": 6,
|
||
"шестерочка": 6,
|
||
"семь": 7,
|
||
"семерка": 7,
|
||
"семерочка": 7,
|
||
"восемь": 8,
|
||
"восьмерка": 8,
|
||
"восьмерочка": 8,
|
||
"девять": 9,
|
||
"девятка": 9,
|
||
"девяточка": 9,
|
||
"десять": 10,
|
||
"десятка": 10,
|
||
"десяточка": 10,
|
||
}
|
||
_VOLUME_COMMAND_RE = re.compile(r"\b(громкост\w*|звук\w*|volume)\b")
|
||
|
||
|
||
def _lemmatize(token: str) -> str:
|
||
if _MORPH is None:
|
||
return token
|
||
return _MORPH.parse(token)[0].normal_form.replace("ё", "е")
|
||
|
||
|
||
def _get_volume_command(level: int):
|
||
"""
|
||
Возвращает команду для изменения громкости в зависимости от ОС.
|
||
|
||
Args:
|
||
level: Уровень громкости (1-10)
|
||
|
||
Returns:
|
||
Список команд для выполнения или None, если команда не поддерживается
|
||
"""
|
||
percentage = level * 10
|
||
|
||
system = platform.system().lower()
|
||
|
||
if system == "linux":
|
||
# Проверяем доступность различных утилит
|
||
if _command_exists("pactl"):
|
||
# Используем PulseAudio (более современный подход)
|
||
return ["pactl", "set-sink-volume", "@DEFAULT_SINK@", f"{percentage}%"]
|
||
elif _command_exists("amixer"):
|
||
# Используем ALSA
|
||
return ["amixer", "-q", "sset", "Master", f"{percentage}%"]
|
||
else:
|
||
# Проверяем alsamixer
|
||
if _command_exists("alsamixer"):
|
||
return ["amixer", "-q", "sset", "Master", f"{percentage}%"]
|
||
elif system == "darwin": # macOS
|
||
return ["osascript", "-e", f"set volume output volume {percentage}"]
|
||
elif system == "windows":
|
||
# Для Windows используем PowerShell команду
|
||
# Это требует дополнительных библиотек, поэтому пока просто покажем сообщение
|
||
print("⚠️ Настройка громкости на Windows требует дополнительных библиотек")
|
||
return None
|
||
|
||
return None
|
||
|
||
|
||
def _command_exists(command):
|
||
"""
|
||
Проверяет, существует ли команда в системе.
|
||
|
||
Args:
|
||
command: Название команды
|
||
|
||
Returns:
|
||
True, если команда существует
|
||
"""
|
||
try:
|
||
result = subprocess.run(
|
||
["which", command],
|
||
stdout=subprocess.DEVNULL,
|
||
stderr=subprocess.DEVNULL,
|
||
check=False,
|
||
)
|
||
return result.returncode == 0
|
||
except:
|
||
try:
|
||
# Альтернативная проверка для Windows
|
||
result = subprocess.run(
|
||
["where", command],
|
||
stdout=subprocess.DEVNULL,
|
||
stderr=subprocess.DEVNULL,
|
||
check=False,
|
||
)
|
||
return result.returncode == 0
|
||
except:
|
||
return False
|
||
|
||
|
||
def set_volume(level: int) -> bool:
|
||
"""
|
||
Устанавливает системную громкость (шкала 1-10).
|
||
1 -> 10%
|
||
10 -> 100%
|
||
|
||
Args:
|
||
level: Число от 1 до 10.
|
||
|
||
Returns:
|
||
True, если успешно.
|
||
"""
|
||
if not isinstance(level, int):
|
||
print(
|
||
f"❌ Ошибка: Уровень громкости должен быть целым числом, получено {type(level)}"
|
||
)
|
||
return False
|
||
|
||
# Ограничение диапазона
|
||
if level < 1:
|
||
level = 1
|
||
elif level > 10:
|
||
level = 10
|
||
|
||
percentage = level * 10
|
||
|
||
# Получаем команду для текущей ОС
|
||
cmd = _get_volume_command(level)
|
||
|
||
if cmd is None:
|
||
print(f"❌ Не найдена подходящая утилита для изменения громкости на вашей системе")
|
||
print(f"💡 Установите PulseAudio (pactl) или ALSA (amixer) для управления громкостью")
|
||
return False
|
||
|
||
try:
|
||
# Выполняем команду
|
||
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||
print(f"🔊 Громкость установлена на {level} ({percentage}%)")
|
||
return True
|
||
except subprocess.CalledProcessError as e:
|
||
print(f"❌ Ошибка при установке громкости: {e}")
|
||
print(f"💡 Убедитесь, что у вас установлены и настроены аудио утилиты (pactl, amixer)")
|
||
return False
|
||
except Exception as e:
|
||
print(f"❌ Неизвестная ошибка громкости: {e}")
|
||
return False
|
||
|
||
|
||
def parse_volume_text(text: str) -> int | None:
|
||
"""
|
||
Пытается найти число громкости в тексте.
|
||
Понимает и цифры ("5"), и слова ("пять").
|
||
"""
|
||
text = replace_roman_numerals(text.lower().replace("ё", "е"))
|
||
|
||
# 1. Ищем цифры в любом месте фразы.
|
||
for match in re.finditer(r"\d+", text):
|
||
value = int(match.group())
|
||
if 1 <= value <= 10:
|
||
return value
|
||
|
||
# 2. Ищем числительные и разговорные формы по леммам:
|
||
# "семерку", "десяточку", "на двух" -> 7, 10, 2.
|
||
for token in re.findall(r"[a-zA-Zа-яА-ЯёЁ]+", text):
|
||
value = NUMBER_MAP.get(_lemmatize(token))
|
||
if value is not None and 1 <= value <= 10:
|
||
return value
|
||
|
||
return None
|
||
|
||
|
||
def is_volume_command(text: str) -> bool:
|
||
if not text:
|
||
return False
|
||
return bool(_VOLUME_COMMAND_RE.search(text.lower().replace("ё", "е")))
|