Files
smart-speaker/app/audio/sound_level.py
2026-04-09 21:03:02 +03:00

211 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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("ё", "е")))