fix: use APInex TTS for voice ideas

This commit is contained in:
admin committed 2026-10-05 01:35:41 +02:00
1 parent 95f9729261
commit 40383eb6e7
5 files changed
+56 -29

No files matched your search

-3
View File
@@ -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
+1 -1
View File
@@ -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] Служебные ответы по-русски, язык текстов постов определяется запросом пользователя
+2 -2
View File
@@ -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`.
+1 -1
View File
@@ -1 +1 @@
0.1.0+14
0.1.0+15
+52 -22
View File
@@ -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(