Files
social-post-writer-bot/bot.py
T

142 lines
5.3 KiB
Python

import asyncio
import io
import logging
import os
from typing import Any
import aiohttp
from aiogram import Bot, Dispatcher, F, Router
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TEST
from aiogram.filters import CommandStart
from aiogram.types import Message
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
router = Router()
APINEX_STT_URL = "https://api.apinex.bond/v1/tools/voice/stt?language=ru"
MAX_AUDIO_BYTES = 20 * 1024 * 1024
def extract_transcript(payload: dict[str, Any]) -> str:
"""Return the first APInex voice/STT transcript, or an empty string."""
channels = payload.get("results", {}).get("channels", [])
for channel in channels:
alternatives = channel.get("alternatives", [])
if alternatives:
transcript = alternatives[0].get("transcript", "")
if transcript and transcript.strip():
return transcript.strip()
return ""
def split_text(text: str, limit: int = 4000) -> list[str]:
"""Split a long transcript into Telegram-safe chunks, preferring word boundaries."""
chunks: list[str] = []
remaining = text.strip()
while len(remaining) > limit:
split_at = remaining.rfind(" ", 0, limit + 1)
if split_at < limit // 2:
split_at = limit
chunks.append(remaining[:split_at].strip())
remaining = remaining[split_at:].strip()
if remaining:
chunks.append(remaining)
return chunks
async def transcribe_audio(audio: bytes, content_type: str, api_key: str) -> str:
timeout = aiohttp.ClientTimeout(total=120)
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": content_type,
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (compatible; SocialPostWriterBot/1.0)",
}
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(APINEX_STT_URL, data=audio, headers=headers) as response:
if response.status != 200:
logger.warning("APInex STT returned HTTP %s", response.status)
raise RuntimeError("APInex STT request failed")
try:
payload = await response.json(content_type=None)
except (aiohttp.ContentTypeError, ValueError) as exc:
raise RuntimeError("APInex STT returned invalid JSON") from exc
return extract_transcript(payload)
@router.message(CommandStart())
async def start(message: Message) -> None:
await message.answer("Тестовый бот запущен. Отправь текст — я повторю его, или голосовое — расшифрую.")
@router.message(F.voice | F.audio)
async def transcribe_voice(message: Message, bot: Bot) -> None:
api_key = os.environ.get("APINEX_API_KEY", "").strip()
if not api_key:
await message.answer("Расшифровка пока не настроена.")
return
audio_file = message.voice or message.audio
if audio_file is None:
return
if audio_file.file_size and audio_file.file_size > MAX_AUDIO_BYTES:
await message.answer("Аудиофайл слишком большой. Максимальный размер — 20 МБ.")
return
content_type = "audio/ogg" if message.voice else (message.audio.mime_type or "audio/mpeg")
await bot.send_chat_action(message.chat.id, "typing")
try:
audio_buffer = io.BytesIO()
await bot.download(audio_file, destination=audio_buffer)
audio = audio_buffer.getvalue()
if not audio:
await message.answer("Не удалось получить аудиофайл.")
return
if len(audio) > MAX_AUDIO_BYTES:
await message.answer("Аудиофайл слишком большой. Максимальный размер — 20 МБ.")
return
transcript = await transcribe_audio(audio, content_type, api_key)
except (aiohttp.ClientError, asyncio.TimeoutError, RuntimeError) as exc:
logger.warning("Voice transcription failed: %s", type(exc).__name__)
await message.answer("Не удалось расшифровать аудио. Попробуй ещё раз позже.")
return
except Exception:
logger.exception("Unexpected voice transcription failure")
await message.answer("Произошла ошибка при обработке аудио.")
return
if not transcript:
await message.answer("Не удалось распознать речь в аудио.")
return
for chunk in split_text(transcript):
await message.answer(chunk)
@router.message(F.text & ~F.text.startswith("/"))
async def echo(message: Message) -> None:
if message.text:
await message.answer(message.text)
async def main() -> None:
token = os.environ.get("BOT_TOKEN", "").strip()
if not token:
raise RuntimeError("BOT_TOKEN is required")
test_env = os.environ.get("BOT_ENV", "test").strip().lower() == "test"
session = AiohttpSession(api=TEST) if test_env else None
bot = Bot(token=token, session=session)
dp = Dispatcher()
dp.include_router(router)
try:
await dp.start_polling(bot)
finally:
await bot.session.close()
if __name__ == "__main__":
asyncio.run(main())