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", } 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())