Improve AI module comments and README
This commit is contained in:
32
README.md
32
README.md
@@ -18,6 +18,8 @@
|
|||||||
`Alexander Smart Speaker` слушает ключевое слово `Alexandr`, распознает речь, маршрутизирует команду в нужный модуль и озвучивает ответ.
|
`Alexander Smart Speaker` слушает ключевое слово `Alexandr`, распознает речь, маршрутизирует команду в нужный модуль и озвучивает ответ.
|
||||||
Проект оптимизирован под русский язык, но поддерживает RU/EN сценарии (включая перевод и mixed-language TTS).
|
Проект оптимизирован под русский язык, но поддерживает RU/EN сценарии (включая перевод и mixed-language TTS).
|
||||||
|
|
||||||
|
Проект собран как локальная голосовая колонка под Linux: активация по wake word, распознавание речи, маршрутизация команд, ответ через AI или встроенные модули и затем озвучка результата.
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
- Активация по wake word `Alexandr` (Porcupine).
|
- Активация по wake word `Alexandr` (Porcupine).
|
||||||
@@ -45,6 +47,13 @@ flowchart TD
|
|||||||
F --> G[Follow-up режим или ожидание wake word]
|
F --> G[Follow-up режим или ожидание wake word]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Что Важно В Этой Реализации
|
||||||
|
|
||||||
|
- Контекст диалога хранится в памяти текущей сессии, поэтому после первого вопроса можно продолжать разговор без потери нити.
|
||||||
|
- Системная роль ассистента и `ROLE_JSON` сохраняются для всех поддерживаемых AI-провайдеров.
|
||||||
|
- Для AI используется строго один активный API key. Если в `.env` оставить несколько ключей, ассистент покажет ошибку конфигурации вместо случайного выбора.
|
||||||
|
- Поддержка провайдеров сделана внутри одного модуля, но с разным форматом запросов для OpenAI-compatible API и Anthropic.
|
||||||
|
|
||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
### 1) Системные зависимости (Ubuntu/Debian)
|
### 1) Системные зависимости (Ubuntu/Debian)
|
||||||
@@ -86,6 +95,19 @@ PORCUPINE_ACCESS_KEY=...
|
|||||||
|
|
||||||
Если одновременно оставить несколько AI API key, ассистент вернет ошибку: он не будет выбирать провайдера наугад.
|
Если одновременно оставить несколько AI API key, ассистент вернет ошибку: он не будет выбирать провайдера наугад.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# правильно
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
# GEMINI_API_KEY=...
|
||||||
|
# ANTHROPIC_API_KEY=...
|
||||||
|
|
||||||
|
# неправильно
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
GEMINI_API_KEY=AIza...
|
||||||
|
```
|
||||||
|
|
||||||
### 4) Запуск
|
### 4) Запуск
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -146,6 +168,15 @@ python run.py
|
|||||||
|
|
||||||
Память текущего диалога, история сообщений и `ROLE_JSON` системной роли сохраняются для всех поддерживаемых AI-провайдеров.
|
Память текущего диалога, история сообщений и `ROLE_JSON` системной роли сохраняются для всех поддерживаемых AI-провайдеров.
|
||||||
|
|
||||||
|
## Как Выбирается AI-Провайдер
|
||||||
|
|
||||||
|
1. Приложение проверяет, какие AI API key реально активны в `.env`.
|
||||||
|
2. Если активен ровно один ключ, используется именно он.
|
||||||
|
3. Если активны несколько ключей, ассистент возвращает ошибку конфигурации.
|
||||||
|
4. Если активных ключей нет, приложение ориентируется на `AI_PROVIDER`, но без ключа работать не сможет.
|
||||||
|
|
||||||
|
Такое поведение сделано специально, чтобы конфигурация была предсказуемой и при демонстрации не возникало скрытого переключения между сервисами.
|
||||||
|
|
||||||
## Полезные команды
|
## Полезные команды
|
||||||
|
|
||||||
| Команда | Что делает |
|
| Команда | Что делает |
|
||||||
@@ -179,6 +210,7 @@ alexander_smart-speaker/
|
|||||||
| STT не распознает речь | `DEEPGRAM_API_KEY`, сетевой доступ, выбранный микрофон |
|
| STT не распознает речь | `DEEPGRAM_API_KEY`, сетевой доступ, выбранный микрофон |
|
||||||
| Нет звука | корректное аудиоустройство и доступность `pactl`/`amixer` |
|
| Нет звука | корректное аудиоустройство и доступность `pactl`/`amixer` |
|
||||||
| Будильник/таймер не звонит | наличие `mpg123` в системе |
|
| Будильник/таймер не звонит | наличие `mpg123` в системе |
|
||||||
|
| Ошибка про несколько AI API | в `.env` должен остаться только один незакомментированный AI ключ |
|
||||||
| Spotify не управляется | заполнены `SPOTIFY_*`, есть активное устройство, Premium-аккаунт |
|
| Spotify не управляется | заполнены `SPOTIFY_*`, есть активное устройство, Premium-аккаунт |
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ _PROVIDER_ALIASES = {
|
|||||||
"zai": "zai",
|
"zai": "zai",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# В реальном .env у пользователя должен быть активен только один AI-ключ.
|
||||||
|
# Поэтому настройки храним в одном словаре, а ниже отдельно проверяем конфликт
|
||||||
|
# конфигурации, чтобы ассистент не делал "лучшее предположение" молча.
|
||||||
_PROVIDER_SETTINGS = {
|
_PROVIDER_SETTINGS = {
|
||||||
"perplexity": {
|
"perplexity": {
|
||||||
"provider": "perplexity",
|
"provider": "perplexity",
|
||||||
@@ -137,6 +140,8 @@ def _normalize_provider_name(provider_name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _get_provider_settings():
|
def _get_provider_settings():
|
||||||
|
# Сначала ищем реально активные ключи. Это главный источник истины:
|
||||||
|
# если ключ один, используем именно его, даже если AI_PROVIDER указан иначе.
|
||||||
configured = [
|
configured = [
|
||||||
cfg
|
cfg
|
||||||
for cfg in _PROVIDER_SETTINGS.values()
|
for cfg in _PROVIDER_SETTINGS.values()
|
||||||
@@ -233,11 +238,13 @@ def _split_system_messages(messages):
|
|||||||
role = "user"
|
role = "user"
|
||||||
chat_messages.append({"role": role, "content": content})
|
chat_messages.append({"role": role, "content": content})
|
||||||
|
|
||||||
|
# Anthropic хранит системную инструкцию отдельно от обычной истории чата.
|
||||||
return "\n\n".join(system_parts), chat_messages
|
return "\n\n".join(system_parts), chat_messages
|
||||||
|
|
||||||
|
|
||||||
def _build_payload(cfg, messages, max_tokens, temperature, stream):
|
def _build_payload(cfg, messages, max_tokens, temperature, stream):
|
||||||
if cfg["protocol"] == "anthropic":
|
if cfg["protocol"] == "anthropic":
|
||||||
|
# У Claude схема чуть отличается: system не живет внутри messages.
|
||||||
system_prompt, chat_messages = _split_system_messages(messages)
|
system_prompt, chat_messages = _split_system_messages(messages)
|
||||||
payload = {
|
payload = {
|
||||||
"model": cfg["model"],
|
"model": cfg["model"],
|
||||||
@@ -303,6 +310,7 @@ def _iter_openai_compatible_stream(response):
|
|||||||
yield content
|
yield content
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Некоторые OpenAI-compatible API присылают контент кусками-объектами.
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
for item in content:
|
for item in content:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
@@ -326,6 +334,7 @@ def _iter_anthropic_stream(response):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
chunk_type = data_json.get("type")
|
chunk_type = data_json.get("type")
|
||||||
|
# Claude отдает поток событиями разных типов, нас интересует только текст.
|
||||||
if chunk_type == "content_block_start":
|
if chunk_type == "content_block_start":
|
||||||
content_block = data_json.get("content_block") or {}
|
content_block = data_json.get("content_block") or {}
|
||||||
text = content_block.get("text")
|
text = content_block.get("text")
|
||||||
@@ -374,6 +383,7 @@ def _send_request(messages, max_tokens, temperature, error_text):
|
|||||||
return config_error
|
return config_error
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Обычный запрос нужен для перевода и мест, где стриминг не требуется.
|
||||||
response = _HTTP.post(
|
response = _HTTP.post(
|
||||||
cfg["api_url"],
|
cfg["api_url"],
|
||||||
headers=_build_headers(cfg),
|
headers=_build_headers(cfg),
|
||||||
@@ -454,6 +464,7 @@ def ask_ai_stream(messages_history: list):
|
|||||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + list(messages_history)
|
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + list(messages_history)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# В голосовом режиме удобнее говорить частями, как только они приходят от API.
|
||||||
response = _HTTP.post(
|
response = _HTTP.post(
|
||||||
cfg["api_url"],
|
cfg["api_url"],
|
||||||
headers=_build_headers(cfg),
|
headers=_build_headers(cfg),
|
||||||
|
|||||||
Reference in New Issue
Block a user