перед переделкой переводчика -vosk models и все упоминания в проекте
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
- Распознавание речи (Deepgram, RU/EN).
|
- Распознавание речи (Deepgram, RU/EN).
|
||||||
- Озвучка (Silero TTS, RU/EN).
|
- Озвучка (Silero TTS, RU/EN).
|
||||||
- Перевод RU↔EN (Perplexity).
|
- Перевод RU↔EN (Perplexity).
|
||||||
- Будильник с локальным распознаванием стоп-команд (Vosk).
|
- Будильник с голосовым отключением.
|
||||||
- Управление громкостью (ALSA amixer).
|
- Управление громкостью (ALSA amixer).
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
@@ -85,7 +85,6 @@ Wake word (Porcupine) ──► STT (Deepgram) ──► Логика коман
|
|||||||
- `ai.py` — запросы к Perplexity (чат и перевод).
|
- `ai.py` — запросы к Perplexity (чат и перевод).
|
||||||
- `cleaner.py` — очистка ответа и преобразование чисел (RU).
|
- `cleaner.py` — очистка ответа и преобразование чисел (RU).
|
||||||
- `alarm.py` — будильник и логика расписания.
|
- `alarm.py` — будильник и логика расписания.
|
||||||
- `local_stt.py` — локальный Vosk для стоп-команд.
|
|
||||||
- `sound_level.py` — управление громкостью.
|
- `sound_level.py` — управление громкостью.
|
||||||
|
|
||||||
## Частые проблемы
|
## Частые проблемы
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
"""
|
|
||||||
Local offline Speech-to-Text module using Vosk.
|
|
||||||
Used for simple command detection (like "stop") without internet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Модуль локального распознавания речи (Vosk).
|
|
||||||
# Работает полностью оффлайн (без интернета).
|
|
||||||
# Используется, когда нужно распознать простые команды (например, "стоп" во время будильника),
|
|
||||||
# чтобы не тратить трафик и время на обращение к облаку.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import pyaudio
|
|
||||||
from vosk import Model, KaldiRecognizer
|
|
||||||
from config import VOSK_MODEL_PATH, SAMPLE_RATE
|
|
||||||
|
|
||||||
|
|
||||||
class LocalRecognizer:
|
|
||||||
"""Класс для работы с Vosk."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.model = None
|
|
||||||
self.rec = None
|
|
||||||
self.pa = None
|
|
||||||
self.stream = None
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
"""Загрузка модели Vosk."""
|
|
||||||
if not os.path.exists(VOSK_MODEL_PATH):
|
|
||||||
print(f"❌ Ошибка: Vosk модель не найдена по пути {VOSK_MODEL_PATH}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("📦 Инициализация локального STT (Vosk)...")
|
|
||||||
|
|
||||||
# Трюк для подавления вывода логов Vosk в консоль (он очень шумный)
|
|
||||||
try:
|
|
||||||
null_fd = os.open(os.devnull, os.O_WRONLY)
|
|
||||||
old_stderr = os.dup(2)
|
|
||||||
sys.stderr.flush()
|
|
||||||
os.dup2(null_fd, 2)
|
|
||||||
os.close(null_fd)
|
|
||||||
|
|
||||||
# Сама загрузка модели
|
|
||||||
self.model = Model(str(VOSK_MODEL_PATH))
|
|
||||||
|
|
||||||
# Возвращаем stderr обратно
|
|
||||||
os.dup2(old_stderr, 2)
|
|
||||||
os.close(old_stderr)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error initializing Vosk: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.rec = KaldiRecognizer(self.model, SAMPLE_RATE)
|
|
||||||
self.pa = pyaudio.PyAudio()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def listen_for_keywords(self, keywords: list, timeout: float = 10.0) -> str:
|
|
||||||
"""
|
|
||||||
Слушает микрофон заданное время и проверяет наличие ключевых слов.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keywords: Список слов, которые мы ждем (например, ["стоп", "хватит"]).
|
|
||||||
timeout: Сколько секунд слушать.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Найденное слово или пустую строку.
|
|
||||||
"""
|
|
||||||
if not self.model:
|
|
||||||
if not self.initialize():
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Открываем поток микрофона
|
|
||||||
try:
|
|
||||||
stream = self.pa.open(
|
|
||||||
format=pyaudio.paInt16,
|
|
||||||
channels=1,
|
|
||||||
rate=SAMPLE_RATE,
|
|
||||||
input=True,
|
|
||||||
frames_per_buffer=4096,
|
|
||||||
)
|
|
||||||
stream.start_stream()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка микрофона: {e}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
print(f"👂 Локальное слушание ожидает: {keywords}")
|
|
||||||
|
|
||||||
detected_text = ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
while time.time() - start_time < timeout:
|
|
||||||
data = stream.read(4096, exception_on_overflow=False)
|
|
||||||
|
|
||||||
# Vosk обрабатывает аудио чанками
|
|
||||||
if self.rec.AcceptWaveform(data):
|
|
||||||
# Полный результат
|
|
||||||
res = json.loads(self.rec.Result())
|
|
||||||
text = res.get("text", "")
|
|
||||||
if text:
|
|
||||||
print(f"📝 Локально: {text}")
|
|
||||||
# Проверяем, есть ли ключевое слово в распознанном тексте
|
|
||||||
for kw in keywords:
|
|
||||||
if kw in text:
|
|
||||||
detected_text = text
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Частичный результат (быстрее, чем полный)
|
|
||||||
res = json.loads(self.rec.PartialResult())
|
|
||||||
partial = res.get("partial", "")
|
|
||||||
if partial:
|
|
||||||
for kw in keywords:
|
|
||||||
if kw in partial:
|
|
||||||
detected_text = partial
|
|
||||||
break
|
|
||||||
|
|
||||||
if detected_text:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
stream.stop_stream()
|
|
||||||
stream.close()
|
|
||||||
|
|
||||||
return detected_text
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
if self.pa:
|
|
||||||
self.pa.terminate()
|
|
||||||
|
|
||||||
|
|
||||||
# Глобальный экземпляр
|
|
||||||
_local_recognizer = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_recognizer():
|
|
||||||
global _local_recognizer
|
|
||||||
if _local_recognizer is None:
|
|
||||||
_local_recognizer = LocalRecognizer()
|
|
||||||
return _local_recognizer
|
|
||||||
|
|
||||||
|
|
||||||
def listen_for_keywords(keywords: list, timeout: float = 5.0) -> str:
|
|
||||||
"""Внешняя функция для поиска ключевых слов."""
|
|
||||||
return get_local_recognizer().listen_for_keywords(keywords, timeout)
|
|
||||||
@@ -133,9 +133,19 @@ class TextToSpeech:
|
|||||||
model = self._load_model("ru")
|
model = self._load_model("ru")
|
||||||
speaker = self.speaker_ru
|
speaker = self.speaker_ru
|
||||||
|
|
||||||
# Проверка наличия спикера в модели (защита от ошибок конфига)
|
# Проверка наличия спикера в модели (защита от ошибок конфига).
|
||||||
if hasattr(model, "speakers") and speaker not in model.speakers:
|
# Для русского языка сохраняем мужской голос по умолчанию.
|
||||||
if model.speakers:
|
if hasattr(model, "speakers") and model.speakers:
|
||||||
|
if language == "ru":
|
||||||
|
male_speakers = ("eugene", "aidar")
|
||||||
|
if speaker not in model.speakers or speaker not in male_speakers:
|
||||||
|
for candidate in male_speakers:
|
||||||
|
if candidate in model.speakers:
|
||||||
|
speaker = candidate
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
speaker = model.speakers[0]
|
||||||
|
elif speaker not in model.speakers:
|
||||||
speaker = model.speakers[0]
|
speaker = model.speakers[0]
|
||||||
|
|
||||||
# Разбиваем текст на куски
|
# Разбиваем текст на куски
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class WakeWordDetector:
|
|||||||
self.audio_stream = None
|
self.audio_stream = None
|
||||||
self.pa = None
|
self.pa = None
|
||||||
self._stream_closed = True # Флаг состояния потока (закрыт/открыт)
|
self._stream_closed = True # Флаг состояния потока (закрыт/открыт)
|
||||||
|
self._last_hit_ts = 0.0
|
||||||
|
self._hit_streak = 0
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Инициализация Porcupine и PyAudio."""
|
"""Инициализация Porcupine и PyAudio."""
|
||||||
@@ -118,6 +120,8 @@ class WakeWordDetector:
|
|||||||
Returns:
|
Returns:
|
||||||
True, если фраза обнаружена прямо сейчас.
|
True, если фраза обнаружена прямо сейчас.
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
if not self.porcupine:
|
if not self.porcupine:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
@@ -131,8 +135,17 @@ class WakeWordDetector:
|
|||||||
|
|
||||||
keyword_index = self.porcupine.process(pcm)
|
keyword_index = self.porcupine.process(pcm)
|
||||||
if keyword_index >= 0:
|
if keyword_index >= 0:
|
||||||
print("🛑 Wake word обнаружен во время ответа!")
|
now = time.time()
|
||||||
return True
|
if now - self._last_hit_ts < 0.6:
|
||||||
|
self._hit_streak += 1
|
||||||
|
else:
|
||||||
|
self._hit_streak = 1
|
||||||
|
self._last_hit_ts = now
|
||||||
|
|
||||||
|
if self._hit_streak >= 2:
|
||||||
|
self._hit_streak = 0
|
||||||
|
print("🛑 Wake word подтвержден во время ответа!")
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ SYSTEM_PROMPT = """Ты — Александр, умный голосовой а
|
|||||||
# Требует возвращать ТОЛЬКО перевод, без лишних слов ("Конечно, вот перевод...").
|
# Требует возвращать ТОЛЬКО перевод, без лишних слов ("Конечно, вот перевод...").
|
||||||
TRANSLATION_SYSTEM_PROMPT = """You are a translation engine.
|
TRANSLATION_SYSTEM_PROMPT = """You are a translation engine.
|
||||||
Translate from {source} to {target}.
|
Translate from {source} to {target}.
|
||||||
Return only the translated text, without quotes, comments, or explanations."""
|
Return only the translated text, without quotes, comments, or explanations.
|
||||||
|
Keep the translation максимально кратким и естественным, без лишних слов."""
|
||||||
|
|
||||||
|
|
||||||
def _send_request(messages, max_tokens, temperature, error_text):
|
def _send_request(messages, max_tokens, temperature, error_text):
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ PORCUPINE_ACCESS_KEY = os.getenv("PORCUPINE_ACCESS_KEY")
|
|||||||
# Путь к файлу модели ключевого слова (.ppn), который лежит в папке assets/models
|
# Путь к файлу модели ключевого слова (.ppn), который лежит в папке assets/models
|
||||||
PORCUPINE_KEYWORD_PATH = BASE_DIR / "assets" / "models" / "Alexandr_en_linux_v4_0_0.ppn"
|
PORCUPINE_KEYWORD_PATH = BASE_DIR / "assets" / "models" / "Alexandr_en_linux_v4_0_0.ppn"
|
||||||
|
|
||||||
# --- Настройки локального распознавания (Vosk) ---
|
|
||||||
# Используется для стоп-команд и будильника, когда не нужен интернет
|
|
||||||
VOSK_MODEL_PATH = BASE_DIR / "assets" / "models" / "vosk-model-ru-0.42"
|
|
||||||
|
|
||||||
# --- Параметры аудио ---
|
# --- Параметры аудио ---
|
||||||
# Частота дискретизации для микрофона (стандарт для распознавания речи)
|
# Частота дискретизации для микрофона (стандарт для распознавания речи)
|
||||||
SAMPLE_RATE = 16000
|
SAMPLE_RATE = 16000
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import re
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ..core.config import BASE_DIR
|
from ..core.config import BASE_DIR
|
||||||
from ..audio.local_stt import listen_for_keywords
|
from ..audio.stt import listen
|
||||||
|
|
||||||
# Файл базы данных будильников
|
# Файл базы данных будильников
|
||||||
ALARM_FILE = BASE_DIR / "data" / "alarms.json"
|
ALARM_FILE = BASE_DIR / "data" / "alarms.json"
|
||||||
@@ -90,7 +90,7 @@ class AlarmClock:
|
|||||||
"""
|
"""
|
||||||
Логика срабатывания будильника.
|
Логика срабатывания будильника.
|
||||||
Запускает воспроизведение MP3 через mpg123 и слушает команду "Стоп".
|
Запускает воспроизведение MP3 через mpg123 и слушает команду "Стоп".
|
||||||
Использует локальное распознавание (Vosk), чтобы не зависеть от интернета.
|
Использует облачное распознавание речи для остановки.
|
||||||
"""
|
"""
|
||||||
print("🔔 БУДИЛЬНИК ЗВОНИТ! (Скажите 'Стоп' или 'Александр стоп')")
|
print("🔔 БУДИЛЬНИК ЗВОНИТ! (Скажите 'Стоп' или 'Александр стоп')")
|
||||||
|
|
||||||
@@ -117,11 +117,12 @@ class AlarmClock:
|
|||||||
|
|
||||||
# Цикл ожидания стоп-команды
|
# Цикл ожидания стоп-команды
|
||||||
while True:
|
while True:
|
||||||
# Слушаем локально (без интернета)
|
text = listen(timeout_seconds=3.0, detection_timeout=3.0)
|
||||||
text = listen_for_keywords(stop_words, timeout=3.0)
|
|
||||||
if text:
|
if text:
|
||||||
print(f"🛑 Будильник остановлен по команде: '{text}'")
|
text_lower = text.lower()
|
||||||
break
|
if any(word in text_lower for word in stop_words):
|
||||||
|
print(f"🛑 Будильник остановлен по команде: '{text}'")
|
||||||
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка во время будильника: {e}")
|
print(f"❌ Ошибка во время будильника: {e}")
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ triton==3.5.1
|
|||||||
typing-inspect==0.9.0
|
typing-inspect==0.9.0
|
||||||
typing_extensions==4.15.0
|
typing_extensions==4.15.0
|
||||||
urllib3==2.6.2
|
urllib3==2.6.2
|
||||||
vosk==0.3.45
|
|
||||||
websockets==15.0.1
|
websockets==15.0.1
|
||||||
yarl==1.22.0
|
yarl==1.22.0
|
||||||
pygame
|
pygame
|
||||||
|
|||||||
Reference in New Issue
Block a user