From c055d46d1591d47caa6737559981526d412617e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 1 Jun 2026 21:20:14 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BC=D1=91=D1=80=D1=82=D0=B2=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D1=8B=20config,=20routes,=20services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/config.py | 87 --------------------------- site/routes/__init__.py | 11 ---- site/routes/api.py | 92 ---------------------------- site/routes/pages.py | 22 ------- site/services/__init__.py | 11 ---- site/services/llm.py | 123 -------------------------------------- site/services/whisper.py | 120 ------------------------------------- 7 files changed, 466 deletions(-) delete mode 100644 site/config.py delete mode 100644 site/routes/__init__.py delete mode 100644 site/routes/api.py delete mode 100644 site/routes/pages.py delete mode 100644 site/services/__init__.py delete mode 100644 site/services/llm.py delete mode 100644 site/services/whisper.py diff --git a/site/config.py b/site/config.py deleted file mode 100644 index 4af27b7..0000000 --- a/site/config.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Централизованная конфигурация приложения Say. - -Все настройки читаются из переменных окружения с разумными значениями по умолчанию. -""" - -import os - -# ============================================================================= -# API — единый эндпоинт (aillm.ru) -# ============================================================================= - -API_BASE = os.environ.get("API_BASE", "https://api.aillm.ru") -API_KEY = os.environ.get("API_KEY", "") # ⚠️ Без ключа — только через env! - -# ============================================================================= -# Whisper (распознавание речи) -# ============================================================================= - -WHISPER_URL = os.environ.get( - "WHISPER_URL", - f"{API_BASE}/v1/audio/transcriptions", -) -WHISPER_KEY = os.environ.get("WHISPER_KEY", API_KEY) -WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "whisper-large-v3") - -# ============================================================================= -# Три LLM-профиля — каждый со своим именем, моделью и системным промптом -# ============================================================================= - -LLM_PROFILES = [ - { - "id": "ddm", - "name": "DDM-120", - "description": "Быстрая модель, общие вопросы", - "url": os.environ.get("LLM_URL_DDM", f"{API_BASE}/v1/chat/completions"), - "key": os.environ.get("LLM_KEY_DDM", API_KEY), - "model": os.environ.get("LLM_MODEL_DDM", "ddm-120"), - "system_prompt": os.environ.get( - "LLM_SYSTEM_DDM", - "Ты — полезный ассистент. Отвечай кратко и по делу на том же языке, что и запрос.", - ), - "temperature": float(os.environ.get("LLM_TEMP_DDM", "0.7")), - "max_tokens": int(os.environ.get("LLM_TOKENS_DDM", "1024")), - }, - { - "id": "qwen", - "name": "Qwen", - "description": "Мощная модель, сложные задачи", - "url": os.environ.get("LLM_URL_QWEN", f"{API_BASE}/v1/chat/completions"), - "key": os.environ.get("LLM_KEY_QWEN", API_KEY), - "model": os.environ.get("LLM_MODEL_QWEN", "qwen-plus"), - "system_prompt": os.environ.get( - "LLM_SYSTEM_QWEN", - "Ты — эксперт-аналитик. Отвечай развернуто, структурированно, на языке запроса.", - ), - "temperature": float(os.environ.get("LLM_TEMP_QWEN", "0.7")), - "max_tokens": int(os.environ.get("LLM_TOKENS_QWEN", "2048")), - }, - { - "id": "deepseek", - "name": "DeepSeek", - "description": "Глубокая аналитика, код, рассуждения", - "url": os.environ.get("LLM_URL_DEEPSEEK", f"{API_BASE}/v1/chat/completions"), - "key": os.environ.get("LLM_KEY_DEEPSEEK", API_KEY), - "model": os.environ.get("LLM_MODEL_DEEPSEEK", "deepseek-chat"), - "system_prompt": os.environ.get( - "LLM_SYSTEM_DEEPSEEK", - "Ты — глубокий аналитик и программист. Думай step-by-step, отвечай на языке запроса.", - ), - "temperature": float(os.environ.get("LLM_TEMP_DEEPSEEK", "0.7")), - "max_tokens": int(os.environ.get("LLM_TOKENS_DEEPSEEK", "4096")), - }, -] - -# ============================================================================= -# Flask -# ============================================================================= - -FLASK_HOST = os.environ.get("HOST", "0.0.0.0") -FLASK_PORT = int(os.environ.get("PORT", "5000")) - -# ============================================================================= -# Логирование -# ============================================================================= - -LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO") diff --git a/site/routes/__init__.py b/site/routes/__init__.py deleted file mode 100644 index 54f9ee8..0000000 --- a/site/routes/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Роуты приложения Say. - -pages.py — HTML-страницы (index, health). -api.py — REST API (список профилей, отправка речи). -""" - -from .pages import pages_bp -from .api import api_bp - -__all__ = ["pages_bp", "api_bp"] diff --git a/site/routes/api.py b/site/routes/api.py deleted file mode 100644 index 773ede2..0000000 --- a/site/routes/api.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -API-роуты. - -GET /api/profiles — список доступных LLM-профилей. -POST /api/speech/ — принять аудио, распознать, отправить в LLM, вернуть ответ. -""" - -import logging - -from flask import Blueprint, request, jsonify - -from services.whisper import whisper_service -from services.llm import llm_service - -log = logging.getLogger(__name__) - -api_bp = Blueprint("api", __name__, url_prefix="/api") - - -@api_bp.route("/profiles") -def get_profiles(): - """Возвращает список LLM-профилей для отображения на фронтенде.""" - return jsonify(llm_service.profile_list) - - -@api_bp.route("/speech/", methods=["POST"]) -def speech_to_llm(llm_id: str): - """ - Основной рабочий процесс: - - 1. Принимает аудиофайл из формы (поле "audio"). - 2. Отправляет в Whisper → получает распознанный текст. - 3. Отправляет текст в выбранный LLM-профиль → получает ответ. - 4. Возвращает JSON: { transcription, response, llm }. - - Args: - llm_id: Идентификатор LLM-профиля (ddm, qwen, deepseek, ...). - """ - # --- Валидация входных данных --- - if "audio" not in request.files: - return jsonify({"error": "Поле 'audio' обязательно"}), 400 - - audio_file = request.files["audio"] - audio_bytes = audio_file.read() - mime_type = audio_file.mimetype or "audio/webm" - - if len(audio_bytes) == 0: - return jsonify({"error": "Аудиофайл пуст"}), 400 - - # --- Проверка существования LLM-профиля --- - profile = llm_service.get_profile(llm_id) - if not profile: - return jsonify({ - "error": f"Неизвестный LLM: '{llm_id}'", - "available": [p["id"] for p in llm_service.profile_list], - }), 400 - - try: - # Шаг 1: Whisper — распознавание речи - log.info("API /speech/%s: получено %d байт аудио", llm_id, len(audio_bytes)) - transcription = whisper_service.transcribe(audio_bytes, mime_type) - - if not transcription: - return jsonify({ - "error": "Не удалось распознать речь", - "transcription": "", - }), 400 - - # Шаг 2: LLM — отправка распознанного текста - llm_answer = llm_service.chat(llm_id, transcription) - - # Шаг 3: Ответ - return jsonify({ - "transcription": transcription, - "response": llm_answer, - "llm": { - "id": profile["id"], - "name": profile["name"], - }, - }) - - except ValueError as e: - log.warning("API /speech/%s: bad request — %s", llm_id, e) - return jsonify({"error": str(e)}), 400 - - except RuntimeError as e: - log.error("API /speech/%s: runtime error — %s", llm_id, e) - return jsonify({"error": str(e)}), 500 - - except Exception as e: - log.exception("API /speech/%s: unexpected error", llm_id) - return jsonify({"error": f"Внутренняя ошибка: {e}"}), 500 diff --git a/site/routes/pages.py b/site/routes/pages.py deleted file mode 100644 index 6a54c6e..0000000 --- a/site/routes/pages.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Роуты HTML-страниц. - -GET / — главная страница (интерфейс с микрофоном). -GET /health — проверка живости сервера. -""" - -from flask import Blueprint, render_template, jsonify - -pages_bp = Blueprint("pages", __name__) - - -@pages_bp.route("/") -def index(): - """Главная страница с тремя кнопками записи (по одной на каждый ЛЛМ).""" - return render_template("index.html") - - -@pages_bp.route("/health") -def health(): - """Проверка работоспособности сервера.""" - return jsonify({"status": "ok"}) diff --git a/site/services/__init__.py b/site/services/__init__.py deleted file mode 100644 index 22f8e08..0000000 --- a/site/services/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Сервисы приложения Say. - -WhisperService — распознавание речи (speech-to-text). -LLMService — обращение к языковой модели (text-to-response). -""" - -from .whisper import WhisperService -from .llm import LLMService - -__all__ = ["WhisperService", "LLMService"] diff --git a/site/services/llm.py b/site/services/llm.py deleted file mode 100644 index 5cca470..0000000 --- a/site/services/llm.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -LLMService — обращение к языковой модели через OpenAI-совместимый API. - -Поддерживает несколько профилей (разные модели/промпты) — -пользователь выбирает, к какому ЛЛМ отправить запрос. -""" - -import logging -from typing import Dict, Optional - -import requests - -from config import LLM_PROFILES - -log = logging.getLogger(__name__) - - -class LLMService: - """ - Сервис общения с языковой моделью. - - Хранит набор профилей (model + system_prompt + температура + ...). - Метод chat() принимает id профиля и текст пользователя, возвращает ответ. - """ - - def __init__(self, profiles: list = None): - """ - Args: - profiles: Список словарей с ключами: - id, name, description, url, key, model, - system_prompt, temperature, max_tokens. - По умолчанию — LLM_PROFILES из config.py. - """ - self._profiles: Dict[str, dict] = {} - - for p in (profiles or LLM_PROFILES): - pid = p["id"] - self._profiles[pid] = p - log.info("LLM: зарегистрирован профиль '%s' (модель=%s)", pid, p["model"]) - - @property - def profile_list(self) -> list: - """ - Возвращает список профилей для фронтенда - (без ключей, только публичная информация). - """ - return [ - { - "id": p["id"], - "name": p["name"], - "description": p["description"], - } - for p in self._profiles.values() - ] - - def get_profile(self, profile_id: str) -> Optional[dict]: - """Возвращает полный профиль по id или None.""" - return self._profiles.get(profile_id) - - def chat(self, profile_id: str, user_text: str) -> str: - """ - Отправляет текст пользователя в указанную LLM и возвращает ответ. - - Args: - profile_id: Идентификатор профиля (ddm, qwen, deepseek, ...). - user_text: Текст, который нужно отправить модели. - - Returns: - Ответ модели (строка). - - Raises: - ValueError: Если профиль не найден. - RuntimeError: При ошибке API. - """ - profile = self._profiles.get(profile_id) - if not profile: - available = ", ".join(self._profiles.keys()) - raise ValueError( - f"Неизвестный LLM-профиль '{profile_id}'. Доступны: {available}" - ) - - if not profile["key"]: - raise RuntimeError(f"API-ключ для профиля '{profile_id}' не задан") - - log.info( - "LLM [%s]: отправка запроса (модель=%s, %d символов)", - profile_id, - profile["model"], - len(user_text), - ) - - resp = requests.post( - profile["url"], - headers={ - "Authorization": f"Bearer {profile['key']}", - "Content-Type": "application/json", - }, - json={ - "model": profile["model"], - "messages": [ - {"role": "system", "content": profile["system_prompt"]}, - {"role": "user", "content": user_text}, - ], - "temperature": profile["temperature"], - "max_tokens": profile["max_tokens"], - }, - timeout=60, - ) - - if resp.status_code != 200: - log.error("LLM [%s] API error %d: %s", profile_id, resp.status_code, resp.text) - raise RuntimeError( - f"Ошибка LLM ({resp.status_code}): {resp.text[:200]}" - ) - - answer = resp.json()["choices"][0]["message"]["content"].strip() - - log.info("LLM [%s]: получен ответ (%d символов)", profile_id, len(answer)) - return answer - - -# Глобальный экземпляр сервиса -llm_service = LLMService() diff --git a/site/services/whisper.py b/site/services/whisper.py deleted file mode 100644 index ef9f0b4..0000000 --- a/site/services/whisper.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -WhisperService — распознавание речи через OpenAI-совместимый API. - -Принимает аудио-байты и MIME-тип, возвращает распознанный текст. -""" - -import logging -import tempfile -from typing import Optional - -import requests - -from config import WHISPER_URL, WHISPER_KEY, WHISPER_MODEL - -log = logging.getLogger(__name__) - - -class WhisperService: - """ - Сервис транскрибации аудио в текст. - - Использует OpenAI-совместимый эндпоинт /v1/audio/transcriptions. - Поддерживает форматы: webm, wav, mp3, ogg, flac. - """ - - def __init__( - self, - api_url: str = WHISPER_URL, - api_key: str = WHISPER_KEY, - model: str = WHISPER_MODEL, - ): - """ - Args: - api_url: URL эндпоинта транскрибации. - api_key: API-ключ (Bearer-токен). - model: Название модели Whisper. - """ - self._url = api_url - self._key = api_key - self._model = model - - def transcribe( - self, - audio_bytes: bytes, - mime_type: str = "audio/webm", - language: str = "ru", - ) -> str: - """ - Отправляет аудио на распознавание и возвращает текст. - - Args: - audio_bytes: Сырые байты аудиофайла. - mime_type: MIME-тип аудио (audio/webm, audio/wav, ...). - language: Язык распознавания (ISO 639-1). - - Returns: - Распознанный текст (может быть пустой строкой). - - Raises: - RuntimeError: При ошибке API или отсутствии ключа. - """ - if not self._key: - raise RuntimeError("API-ключ для Whisper не задан (WHISPER_KEY)") - - # Определяем расширение файла по MIME-типу - suffix = self._mime_to_suffix(mime_type) - - # Пишем аудио во временный файл (requests требует file-like object) - with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: - tmp.write(audio_bytes) - tmp.flush() - tmp.seek(0) - - log.info( - "Whisper: отправка %d байт (тип=%s, модель=%s)", - len(audio_bytes), - mime_type, - self._model, - ) - - resp = requests.post( - self._url, - headers={"Authorization": f"Bearer {self._key}"}, - files={"file": (f"audio{suffix}", tmp, mime_type)}, - data={ - "model": self._model, - "language": language, - }, - timeout=30, - ) - - if resp.status_code != 200: - log.error("Whisper API error %d: %s", resp.status_code, resp.text) - raise RuntimeError( - f"Ошибка распознавания ({resp.status_code}): {resp.text[:200]}" - ) - - result = resp.json() - text = result.get("text", "").strip() - - log.info("Whisper: распознано %d символов", len(text)) - return text - - @staticmethod - def _mime_to_suffix(mime_type: str) -> str: - """Сопоставляет MIME-тип с расширением файла.""" - mapping = { - "audio/webm": ".webm", - "audio/wav": ".wav", - "audio/wave": ".wav", - "audio/mp3": ".mp3", - "audio/mpeg": ".mp3", - "audio/ogg": ".ogg", - "audio/flac": ".flac", - } - return mapping.get(mime_type, ".webm") - - -# Глобальный экземпляр сервиса (переиспользуется между запросами) -whisper_service = WhisperService()