fix: use APInex TTS for voice ideas
This commit is contained in:
1 parent
95f9729261
commit
40383eb6e7
5 files changed
+56
-29
No files matched your search
@@ -3,9 +3,6 @@ FROM python:3.12-slim
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends espeak-ng lame \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
@@ -53,7 +53,7 @@ Telegram-бот, который по запросу пользователя г
|
||||
- [x] APInex JSON-вызовы и ошибки разбора журналируются в SQLite без системного промпта и image base64; срок хранения — 30 дней
|
||||
- [x] `/clear` полностью очищает данные текущего пользователя: ToV, скриншоты, историю постов, историю идей и настройки; API-логи сохраняются
|
||||
- [x] `/idea` возвращает одну идею одним сообщением и сохраняет её для исключения повторов
|
||||
- [x] `/idea_voice` озвучивает одну идею локальным русским TTS (`espeak-ng` + `lame`) и отправляет MP3 как voice message; Docker runtime включает оба инструмента
|
||||
- [x] `/idea_voice` использует APInex TTS (`APINEX_TTS_MODEL`, default `aura-2-thalia-en`) и отправляет MP3 voice message; в API-логах только метаданные, соответствие русского голоса проверяется отдельно
|
||||
- [x] Все отправленные варианты постов сохраняются по пользователю, включая исправленные; история используется для новых идей
|
||||
- [x] `/clear` очищает историю пользовательских данных и оставляет API-логи
|
||||
- [x] Служебные ответы по-русски, язык текстов постов определяется запросом пользователя
|
||||
|
||||
@@ -7,11 +7,11 @@ The bot prepares social-media posts only when the user explicitly asks. It trans
|
||||
- In development, processing errors are returned to the user with a concise, secret-redacted reason.
|
||||
- Coolify: project `Social Post Writer Bot`, environment `development`, server `spb-easy-dale`
|
||||
- Gitea: `admin/social-post-writer-bot`, branch `dev`
|
||||
- API key and model variables are stored only in Coolify: `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`.
|
||||
- API key and model variables are stored only in Coolify: `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `APINEX_TTS_MODEL`.
|
||||
- APInex call and parsing logs live in SQLite at `/app/data/social_writer.sqlite3` for 30 days. They store user input/output JSON and error details; system prompt text is omitted in favor of a prompt name and SHA-256. Inline image base64 and binary audio are omitted. Treat log contents as private user data.
|
||||
- `/clear` полностью очищает данные текущего пользователя: Tone of Voice, тексты со скриншотов, историю постов, историю идей и настройки; API-логи сохраняются.
|
||||
- `/idea` и русские запросы на идеи возвращают одну идею одним сообщением и сохраняют её для исключения повторов.
|
||||
- `/idea_voice` генерирует одну идею и отправляет её голосовым MP3-сообщением; Docker image включает `espeak-ng` и `lame` для локального русского TTS.
|
||||
- `/idea_voice` озвучивает одну идею через APInex TTS (`/v1/tools/voice/tts`) и отправляет MP3 как Telegram voice message. Голос задаётся переменной Coolify `APINEX_TTS_MODEL` (по умолчанию `aura-2-thalia-en`). В опубликованном каталоге APInex русский голос пока не указан; качество русской речи отдельно проверим следующим шагом.
|
||||
- User-facing service text is always Russian; generated post variants follow the requested post language. Verbatim technical error detail may remain in its original language.
|
||||
- LLM model env defaults are `gemini-3.8-flash`; STT model defaults to APInex `nova-2`.
|
||||
- Per-user Tone of Voice lives in SQLite at `/app/data/social_writer.sqlite3`; persistent Coolify storage must be mounted at `/app/data`.
|
||||
|
||||
+52
-22
@@ -5,9 +5,6 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
@@ -17,6 +14,7 @@ from user_store import save_api_call_log
|
||||
logger = logging.getLogger(__name__)
|
||||
APINEX_STT_URL = "https://api.apinex.bond/v1/tools/voice/stt"
|
||||
APINEX_CHAT_URL = "https://api.apinex.bond/v1/chat/completions"
|
||||
APINEX_TTS_URL = "https://api.apinex.bond/v1/tools/voice/tts"
|
||||
TTS_NOT_CONFIGURED_MESSAGE = "Озвучка идей пока не настроена."
|
||||
USER_AGENT = "Mozilla/5.0 (compatible; SocialPostWriterBot/1.0)"
|
||||
|
||||
@@ -414,26 +412,58 @@ def _trim_history_posts(posts: list[str], char_limit: int = POST_HISTORY_CHAR_LI
|
||||
|
||||
|
||||
async def synthesize_idea_voice(text: str, user_id: int | None = None) -> bytes:
|
||||
"""Generate Russian MP3 speech locally; Telegram accepts MP3 in voice messages."""
|
||||
espeak = shutil.which("espeak-ng") or shutil.which("espeak")
|
||||
lame = shutil.which("lame")
|
||||
if not espeak or not lame:
|
||||
raise RuntimeError("Локальная озвучка не настроена: отсутствует espeak-ng или lame")
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
wav_path = os.path.join(temp_dir, "idea.wav")
|
||||
mp3_path = os.path.join(temp_dir, "idea.mp3")
|
||||
await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
[espeak, "-v", "ru", "-s", "155", "-w", wav_path, text[:1200]],
|
||||
check=True, capture_output=True, timeout=30,
|
||||
api_key = _api_key()
|
||||
if not api_key:
|
||||
raise RuntimeError("APINEX_API_KEY is missing")
|
||||
model = _setting("APINEX_TTS_MODEL", "aura-2-thalia-en")
|
||||
payload = {
|
||||
"text": text[:1200],
|
||||
"model": model,
|
||||
"encoding": "mp3",
|
||||
"container": "mp3",
|
||||
"response_format": "audio",
|
||||
}
|
||||
request_for_log = {
|
||||
"endpoint": "tools/voice/tts",
|
||||
"model": model,
|
||||
"text": payload["text"],
|
||||
"encoding": "mp3",
|
||||
"container": "mp3",
|
||||
"response_format": "audio",
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Accept": "audio/mpeg, application/octet-stream",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": USER_AGENT,
|
||||
}
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=120)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.post(APINEX_TTS_URL, json=payload, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
raise RuntimeError(f"APInex TTS failed with HTTP {response.status}")
|
||||
audio = await response.read()
|
||||
response_meta = {
|
||||
"content_type": response.headers.get("Content-Type", ""),
|
||||
"audio_bytes": len(audio),
|
||||
}
|
||||
if not audio:
|
||||
raise RuntimeError("APInex TTS returned empty audio")
|
||||
except Exception as exc:
|
||||
await _record_api_call(
|
||||
stage="idea_voice_tts", model=model, prompt_name="idea_voice_tts",
|
||||
system_prompt="", request=request_for_log,
|
||||
status_code=_status_from_error(exc), error=f"{type(exc).__name__}: {exc}",
|
||||
user_id=user_id,
|
||||
)
|
||||
await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
[lame, "--silent", "-b", "64", wav_path, mp3_path],
|
||||
check=True, capture_output=True, timeout=30,
|
||||
)
|
||||
with open(mp3_path, "rb") as audio_file:
|
||||
return await asyncio.to_thread(audio_file.read)
|
||||
raise
|
||||
await _record_api_call(
|
||||
stage="idea_voice_tts", model=model, prompt_name="idea_voice_tts",
|
||||
system_prompt="", request=request_for_log, response=response_meta,
|
||||
status_code=200, user_id=user_id,
|
||||
)
|
||||
return audio
|
||||
|
||||
|
||||
async def generate_ideas(
|
||||
|
||||
Reference in new issue
Block a user