feat: show typing status during processing
This commit is contained in:
1 parent
6abea3acc8
commit
33cc2b8a15
4 files changed
+116
-52
No files matched your search
@@ -2,6 +2,7 @@ import asyncio
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager, nullcontext
|
||||
|
||||
import aiohttp
|
||||
from aiogram import Bot, Dispatcher, F, Router
|
||||
@@ -19,6 +20,7 @@ logger = logging.getLogger(__name__)
|
||||
router = Router()
|
||||
MAX_AUDIO_BYTES = 20 * 1024 * 1024
|
||||
MAX_IMAGE_BYTES = 10 * 1024 * 1024
|
||||
TYPING_REFRESH_SECONDS = 4
|
||||
|
||||
|
||||
def split_text(text: str, limit: int = 4000) -> list[str]:
|
||||
@@ -48,7 +50,33 @@ async def _send_three_posts(message: Message, posts: list[str]) -> None:
|
||||
await message.answer(f"{index}/3\n{post}")
|
||||
|
||||
|
||||
async def _classify_and_respond(message: Message, text: str) -> None:
|
||||
@asynccontextmanager
|
||||
async def typing_status(bot: Bot, chat_id: int):
|
||||
try:
|
||||
await bot.send_chat_action(chat_id, "typing")
|
||||
except Exception as exc:
|
||||
logger.debug("Typing indicator unavailable: %s", type(exc).__name__)
|
||||
|
||||
async def refresh_typing() -> None:
|
||||
while True:
|
||||
await asyncio.sleep(TYPING_REFRESH_SECONDS)
|
||||
try:
|
||||
await bot.send_chat_action(chat_id, "typing")
|
||||
except Exception as exc:
|
||||
logger.debug("Typing indicator refresh failed: %s", type(exc).__name__)
|
||||
|
||||
task = asyncio.create_task(refresh_typing())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
async def _classify_and_respond(message: Message, text: str, show_typing: bool = True) -> None:
|
||||
if message.from_user is None:
|
||||
await message.answer("Не удалось определить пользователя.")
|
||||
return
|
||||
@@ -57,22 +85,23 @@ async def _classify_and_respond(message: Message, text: str) -> None:
|
||||
if reply_message and reply_message.from_user and reply_message.from_user.is_bot:
|
||||
original_post = (reply_message.text or reply_message.caption or "").strip()
|
||||
try:
|
||||
intent = await classify_intent(text, original_post or None)
|
||||
if intent["intent"] == "other":
|
||||
await message.answer("Я помогаю только писать и редактировать посты для социальных сетей.")
|
||||
return
|
||||
if intent["intent"] == "revise_post":
|
||||
if not original_post:
|
||||
await message.answer("Чтобы исправить пост, ответь реплаем на сообщение с его вариантом.")
|
||||
async with (typing_status(message.bot, message.chat.id) if show_typing else nullcontext()):
|
||||
intent = await classify_intent(text, original_post or None)
|
||||
if intent["intent"] == "other":
|
||||
await message.answer("Я помогаю только писать и редактировать посты для социальных сетей.")
|
||||
return
|
||||
tone = await asyncio.to_thread(get_tone_of_voice, message.from_user.id)
|
||||
posts = await generate_posts(
|
||||
intent, tone, original_post=original_post, edit_request=text,
|
||||
)
|
||||
else:
|
||||
tone = await asyncio.to_thread(get_tone_of_voice, message.from_user.id)
|
||||
posts = await generate_posts(intent, tone)
|
||||
await _send_three_posts(message, posts)
|
||||
if intent["intent"] == "revise_post":
|
||||
if not original_post:
|
||||
await message.answer("Чтобы исправить пост, ответь реплаем на сообщение с его вариантом.")
|
||||
return
|
||||
tone = await asyncio.to_thread(get_tone_of_voice, message.from_user.id)
|
||||
posts = await generate_posts(
|
||||
intent, tone, original_post=original_post, edit_request=text,
|
||||
)
|
||||
else:
|
||||
tone = await asyncio.to_thread(get_tone_of_voice, message.from_user.id)
|
||||
posts = await generate_posts(intent, tone)
|
||||
await _send_three_posts(message, posts)
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError, RuntimeError, ValueError) as exc:
|
||||
logger.warning("Post request failed: %s", type(exc).__name__)
|
||||
await message.answer("Не получилось подготовить посты. Попробуй ещё раз позже.")
|
||||
@@ -108,23 +137,28 @@ async def _download_message_image(message: Message, bot: Bot) -> tuple[bytes, st
|
||||
return image, mime_type
|
||||
|
||||
|
||||
@router.message(F.photo | (F.document & F.document.mime_type.startswith("image/")))
|
||||
async def update_tone_from_image(message: Message, bot: Bot) -> None:
|
||||
async def _process_image(message: Message, bot: Bot) -> None:
|
||||
if message.from_user is None:
|
||||
await message.answer("Не удалось определить пользователя.")
|
||||
return
|
||||
image_payload = await _download_message_image(message, bot)
|
||||
if image_payload is None:
|
||||
if message.document and not (message.document.mime_type or "").lower().startswith("image/"):
|
||||
await message.answer("Пришли скриншот как изображение, чтобы настроить мой стиль.")
|
||||
return
|
||||
tone = await extract_tone_from_image(*image_payload)
|
||||
if not tone:
|
||||
await message.answer("Не смогла прочитать на изображении пост для соцсетей. Пришли более чёткий скриншот.")
|
||||
return
|
||||
await asyncio.to_thread(save_tone_of_voice, message.from_user.id, tone)
|
||||
await message.answer("Запомнила стиль твоих постов. Буду учитывать его в новых публикациях.")
|
||||
|
||||
|
||||
@router.message(F.photo | (F.document & F.document.mime_type.startswith("image/")))
|
||||
async def update_tone_from_image(message: Message, bot: Bot) -> None:
|
||||
try:
|
||||
image_payload = await _download_message_image(message, bot)
|
||||
if image_payload is None:
|
||||
if message.document and not (message.document.mime_type or "").lower().startswith("image/"):
|
||||
await message.answer("Пришли скриншот как изображение, чтобы настроить мой стиль.")
|
||||
return
|
||||
tone = await extract_tone_from_image(*image_payload)
|
||||
if not tone:
|
||||
await message.answer("Не смогла прочитать на изображении пост для соцсетей. Пришли более чёткий скриншот.")
|
||||
return
|
||||
await asyncio.to_thread(save_tone_of_voice, message.from_user.id, tone)
|
||||
await message.answer("Запомнила стиль твоих постов. Буду учитывать его в новых публикациях.")
|
||||
async with typing_status(bot, message.chat.id):
|
||||
await _process_image(message, bot)
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError, RuntimeError, ValueError) as exc:
|
||||
logger.warning("Tone of voice update failed: %s", type(exc).__name__)
|
||||
await message.answer("Не получилось прочитать скриншот. Попробуй отправить его ещё раз.")
|
||||
@@ -133,36 +167,38 @@ async def update_tone_from_image(message: Message, bot: Bot) -> None:
|
||||
await message.answer("Произошла ошибка при чтении скриншота.")
|
||||
|
||||
|
||||
@router.message(F.voice | F.audio)
|
||||
async def transcribe_voice(message: Message, bot: Bot) -> None:
|
||||
async def _process_voice(message: Message, bot: Bot) -> None:
|
||||
if not os.environ.get("APINEX_API_KEY", "").strip():
|
||||
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
|
||||
|
||||
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
|
||||
content_type = "audio/ogg" if message.voice else (message.audio.mime_type or "audio/mpeg")
|
||||
await bot.send_chat_action(message.chat.id, "typing")
|
||||
transcript = await transcribe_audio(audio, content_type)
|
||||
if not transcript:
|
||||
await message.answer("Не удалось распознать речь в аудио.")
|
||||
return
|
||||
await _classify_and_respond(message, transcript, show_typing=False)
|
||||
|
||||
|
||||
@router.message(F.voice | F.audio)
|
||||
async def transcribe_voice(message: Message, bot: Bot) -> None:
|
||||
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)
|
||||
if not transcript:
|
||||
await message.answer("Не удалось распознать речь в аудио.")
|
||||
return
|
||||
await _classify_and_respond(message, transcript)
|
||||
async with typing_status(bot, message.chat.id):
|
||||
await _process_voice(message, bot)
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError, RuntimeError, ValueError) as exc:
|
||||
logger.warning("Voice post request failed: %s", type(exc).__name__)
|
||||
await message.answer("Не удалось обработать голосовое. Попробуй ещё раз или напиши текстом.")
|
||||
|
||||
Reference in new issue
Block a user