From 2941cb2a24dcbf76f2f0fa960b6bd9fe34051323 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 5 Oct 2026 15:25:22 +0200 Subject: [PATCH] feat: remove /idea_voice, revert to text-only ideas --- .hermes/plans/2026-10-04_api-json-logs.md | 27 +++++++++ .../2026-10-04_idea-command-post-history.md | 32 +++++++++++ ...26-10-04_idea-history-separate-messages.md | 24 ++++++++ .hermes/plans/2026-10-04_idea-voice.md | 24 ++++++++ .../2026-10-04_tov-screenshot-history.md | 30 ++++++++++ IDEA.MD | 3 +- README.md | 3 +- VERSION | 2 +- apinex_client.py | 57 ------------------- bot.py | 28 +-------- help_message.py | 1 - tests/test_release_help.py | 4 +- tests/test_writer_features.py | 41 +++---------- 13 files changed, 152 insertions(+), 124 deletions(-) create mode 100644 .hermes/plans/2026-10-04_api-json-logs.md create mode 100644 .hermes/plans/2026-10-04_idea-command-post-history.md create mode 100644 .hermes/plans/2026-10-04_idea-history-separate-messages.md create mode 100644 .hermes/plans/2026-10-04_idea-voice.md create mode 100644 .hermes/plans/2026-10-04_tov-screenshot-history.md diff --git a/.hermes/plans/2026-10-04_api-json-logs.md b/.hermes/plans/2026-10-04_api-json-logs.md new file mode 100644 index 0000000..19d3897 --- /dev/null +++ b/.hermes/plans/2026-10-04_api-json-logs.md @@ -0,0 +1,27 @@ +# План: сохранение JSON API-логов в SQLite + +## Цель +Сохранять структуру запросов на этапах классификации, генерации, распознавания постов со скриншотов и обновления Tone of Voice, чтобы расследовать ошибки по фактическим данным. + +## Подход +Сохранять в SQLite JSON входных данных и ответ/ошибку каждого этапа, но не дублировать большие неизменные system prompts в каждом логе: хранить промпты как именованные версии с SHA-256 и записывать в лог только имя/версию/хэш. В каждом вызове сохранять переменные пользовательские поля и ответ модели; образцы скриншотов могут храниться в своей таблице. При чтении сначала находить ошибочные вызовы и выводить этап, время, prompt id/hash, размеры и краткие поля; полный JSON конкретного вызова запрашивать отдельно по ID. Не сохранять API-ключи/заголовки, бинарное аудио и base64 изображения; base64 заменять MIME-типом и размером. Учитывать, что пользовательские тексты и ответы модели могут содержать личные данные. + +## Шаги +1. Добавить таблицу логов API-запросов и функции сохранения/чтения с ограничением размера записей и сроком хранения. +2. Обернуть APInex chat/STT вызовы журналированием JSON-запроса и ответа/ошибки без раскрытия секретов; присвоить каждому вызову понятное имя этапа. +3. Добавить тесты на все этапы, ошибочные ответы, отсутствие секретов/base64 и сохранение/очистку логов. +4. Добавить постоянное правило языка для пользовательских служебных ответов: бот всегда обращается к пользователю по-русски, включая уточняющие вопросы и подтверждения; исключение — дословный текст технической ошибки. Содержимое самих постов остаётся на языке запроса/требований площадки. +5. Обновить README/IDEA.MD и VERSION. +6. Добавить личную команду `/clear`: удалить только ToV и накопленные распознанные примеры пользователя, затем попросить прислать новые скриншоты. Уточнить, что `/clear` не удаляет API-логи и настройки других пользователей. +7. Запустить полный набор тестов и `git diff --check`. + +## Файлы +- `apinex_client.py` +- `user_store.py` +- `tests/test_writer_features.py` +- `README.md` +- `IDEA.MD` +- `VERSION` + +## Приватность +В БД будут сохраняться тексты запросов пользователей, промпты и ответы модели, необходимые для отладки. Бинарные файлы, base64 содержимое изображений и API-ключи не сохраняются. Планируемое хранение логов — 30 дней; коммит, push и внешний deploy не входят в задачу. diff --git a/.hermes/plans/2026-10-04_idea-command-post-history.md b/.hermes/plans/2026-10-04_idea-command-post-history.md new file mode 100644 index 0000000..eeac3bb --- /dev/null +++ b/.hermes/plans/2026-10-04_idea-command-post-history.md @@ -0,0 +1,32 @@ +# План: идеи публикаций и история созданных постов + +## Цель +Добавить `/idea` и русские естественные запросы для получения тем/заготовок идей, а не готовых текстов постов. Сохранять все созданные ботом варианты, в том числе результаты правок, и учитывать недавние публикации при выдаче новых идей. + +## Подход +В существующую SQLite добавить историю генераций, разделённую по Telegram user id и помеченную как обычная генерация или правка. Каждую выданную версию сохранять отдельно, чтобы исправленные варианты не затирали предыдущие. Для идей передавать модели ограниченную выборку недавних текстов и просить новые темы/углы, избегая повторов; выдавать только идеи, не полный текст публикации. Постоянные правила и ToV не смешивать с данными пользователя. + +## Шаги +1. Добавить намерение `generate_ideas` в классификатор и специальный JSON-формат идей в системные инструкции. +2. Добавить функцию генерации идей: учитывать площадку/тему/запрос и ограниченную историю постов текущего пользователя; валидировать число и непустое содержимое идей. +3. Добавить таблицу истории постов и функции сохранения/получения. Сохранять каждый из трёх вариантов после успешной генерации и каждую новую редакцию, не заменяя старые записи. +4. Подключить `/idea`, а также русские фразы вроде «предложи идеи для постов», «о чём написать пост», «накидай темы» к тому же намерению. Отвечать только идеями, без готовых постов. +5. Обновить `/help`, README и IDEA.MD; поднять `VERSION`. Уточнить в коде и документации: существующая `/clear` сбрасывает только ToV и скриншотные образцы, не историю созданных постов и не API-логи. +6. Добавить тесты на намерение/синонимы, отсутствие готового поста в ответе идеи, сохранение первоначальных и исправленных вариантов, изоляцию по пользователям и использование истории без отправки её целиком при превышении лимита. +7. Запустить полный набор тестов и `git diff --check`. + +## Файлы +- `apinex_client.py` +- `user_store.py` +- `bot.py` +- `help_message.py` +- `tests/test_writer_features.py` +- `tests/test_release_help.py` +- `README.md` +- `IDEA.MD` +- `VERSION` + +## Ограничения +Хранить сами тексты вариантов, которые бот отправил, вместе с типом (`generated`/`revised`) и временем; бинарные вложения не хранить. Для выдачи идей брать последние 30 вариантов или примерно 12 000 символов (что наступит раньше), не отдавая всю базу модели. Записи разделены по пользователям. `/clear` остаётся командой сброса только ToV и распознанных со скриншотов образцов; история вариантов и API-логи сохраняются. + +Коммит, push и внешний deploy не входят в работу. diff --git a/.hermes/plans/2026-10-04_idea-history-separate-messages.md b/.hermes/plans/2026-10-04_idea-history-separate-messages.md new file mode 100644 index 0000000..e6d0f78 --- /dev/null +++ b/.hermes/plans/2026-10-04_idea-history-separate-messages.md @@ -0,0 +1,24 @@ +# План: выдача и накопление идей по одной + +## Цель +Команда `/idea` и русские запросы на идеи должны отправять **одну идею одним Telegram-сообщением** за один запрос. Выданные идеи сохраняются по пользователю и передаются следующему запросу, чтобы не повторять уже предложенные темы и углы. + +Команда `/clear` полностью обнуляет данные бота для текущего пользователя: Tone of Voice, распознанные примеры со скриншотов, историю созданных постов, историю идей и пользовательские настройки. Технические API-логи сохраняются только для отладки. + +## Подход +Генератор будет возвращать ровно одну новую идею в JSON. Бот сохранит её в SQLite и отправит одним сообщением без нумерации. История постов и идей передаётся в ограниченном объёме, чтобы исключать повторы. + +## Шаги +1. Добавить SQLite-хранилище идей: сохранение каждой идеи и получение последних идей текущего пользователя. +2. Изменить `clear_user_tone_data` в полную очистку пользовательских данных: ToV, скриншотные образцы, историю созданных постов, историю идей и строку пользователя; API-логи не удалять. +3. Изменить Telegram-обработчик: сохранять одну идею и отправлять её одним сообщением без нумерации. +4. Обновить `/help`, README, IDEA.MD и VERSION. +5. Добавить тесты на изоляцию пользователей, накопление одной идеи, отдельное сообщение без нумерации и полную очистку `/clear` при сохранении API-логов. +6. Запустить полный набор тестов, компиляцию и `git diff --check`. +7. Закоммитить, отправить в Gitea `dev`, запустить Coolify deploy и проверить статус `finished`. + +## Файлы +`user_store.py`, `apinex_client.py`, `bot.py`, `help_message.py`, `tests/test_writer_features.py`, `tests/test_release_help.py`, `README.md`, `IDEA.MD`, `VERSION`. + +## Ограничения +История идей разделена по Telegram user id. В LLM передаются только ограниченные последние записи; системные промпты не меняются пользовательскими данными. Идеи не превращаются в готовые посты автоматически. diff --git a/.hermes/plans/2026-10-04_idea-voice.md b/.hermes/plans/2026-10-04_idea-voice.md new file mode 100644 index 0000000..4eb5dd5 --- /dev/null +++ b/.hermes/plans/2026-10-04_idea-voice.md @@ -0,0 +1,24 @@ +# План: /idea_voice — озвучить идею одним аудиосообщением + +## Цель +Добавить англоязычную slash-команду `/idea_voice` как голосовой вариант `/idea`: сгенерировать одну идею по истории пользователя, сохранить её и отправить одним Telegram voice message. + +## Pipeline +1. `/idea_voice` → загрузить user-scoped ToV и ограниченную историю постов/идей. +2. Сгенерировать ровно одну идею структурированного вида `{title, angle, why}`; после успеха сохранить в историю. +3. Сформировать короткий естественный русский текст идеи и озвучить его через настроенный TTS-провайдер. +4. Отправить аудио как Telegram voice message. При ошибке TTS не подменять молча результат текстом: вернуть короткую русскую ошибку, сохранив уже созданную идею в истории. + +## До реализации +Проверить существующий `/root/.hermes/skills/hermes-voice-tts-stt` и фактическую конфигурацию провайдера TTS в текущем проекте. Не предполагать, что в проекте уже есть TTS: в видимых файлах есть STT, но подтверждённого исходящего синтеза речи пока не обнаружено. Если доступного TTS API/креденшела/модели нет, остановиться и запросить решение Николая до изменения файлов и новых внешних интеграций. + +## Файлы при доступном TTS +- `bot.py` +- `apinex_client.py` или отдельный клиент TTS, если именно так устроен подтверждённый провайдер +- `requirements.txt`/`Dockerfile` только если API этого требует +- `user_store.py` (только если потребуется менять существующий flow идей) +- `tests/test_writer_features.py`, `tests/test_release_help.py` +- `README.md`, `IDEA.MD`, `VERSION` + +## Верификация +Unit-тесты payload TTS, ошибок API, корректной отправки `voice`/OGG и сохранения одной идеи; полный unittest suite; py_compile; git diff --check; commit/push `dev`; Coolify deploy и проверка finished; затем live-smoke TTS и доступности новой команды. Не утверждать end-to-end без фактической проверки отправки voice в Telegram. diff --git a/.hermes/plans/2026-10-04_tov-screenshot-history.md b/.hermes/plans/2026-10-04_tov-screenshot-history.md new file mode 100644 index 0000000..75fc75f --- /dev/null +++ b/.hermes/plans/2026-10-04_tov-screenshot-history.md @@ -0,0 +1,30 @@ +# План: накопление текстов со скриншотов для Tone of Voice + +## Цель +Новые скриншоты обновляют профиль Tone of Voice, не заменяя контекст предыдущих примеров. Бот не переписывает и не меняет сами посты со скриншотов. + +## Подход +Извлекать текст публикаций со скриншотов отдельно от их стилевых признаков. Хранить распознанные примеры отдельно по Telegram user id в существующей SQLite; при новой загрузке добавлять новые извлечённые тексты к сохранённым и строить обновлённый ToV по накопленному корпусу. Постоянная редакторская инструкция остаётся неизменной, ToV и корпус передаются модели как пользовательские данные, а не добавляются в system prompt. + +## Шаги +1. Обновить vision-инструкцию и интерфейс APInex: получать структурированный результат с распознанными текстами постов и признаками стиля; не генерировать и не переписывать публикации. +2. Добавить SQLite-хранилище истории текстов по пользователю с безопасной миграцией существующей БД; сохранить текущий профиль ToV и его пользовательскую изоляцию. +3. Изменить обработчик скриншотов: загрузить старые примеры, добавить новые без потери старых, обновить профиль ToV по совокупности текстов/скриншотов и сообщить пользователю, что обновлён только стиль. +4. Добавить тесты на накопление, изоляцию пользователей, миграцию старой схемы, отсутствие перезаписи примеров и передачу неизменяемого system prompt. +5. Обновить README и IDEA.MD, поднять VERSION. + +## Файлы +- `apinex_client.py` +- `user_store.py` +- `bot.py` +- `tests/test_writer_features.py` +- `README.md` +- `IDEA.MD` +- `VERSION` + +## Проверка +- Полный прогон `.venv/bin/python -m unittest discover -s tests -v`. +- `git diff --check` и просмотр итогового diff. + +## Ограничение +Храним только распознанный текст и профиль стиля, не бинарные изображения. Скриншоты и их текст считаются недоверенным содержимым: команды с них не исполняются, факты не переносятся в новые посты. Внешний деплой, коммит и push не входят в этот план. diff --git a/IDEA.MD b/IDEA.MD index dbec49c..a47a4c9 100644 --- a/IDEA.MD +++ b/IDEA.MD @@ -37,7 +37,7 @@ Telegram-бот, который по запросу пользователя г ## Статус - [x] Идея проекта зафиксирована -- [x] MVP: генерация трёх вариантов поста по явной просьбе, включая голосовую +- [x] MVP: генерация трёх вариантов поста по явной просьбе, включая голосовую расшифровку - [x] Классификатор намерений и Tone of Voice из скриншота - [x] SQLite-настройки пользователей и reply-правки без хранения диалога - [x] Development-развёртывание в Coolify: `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) @@ -53,7 +53,6 @@ Telegram-бот, который по запросу пользователя г - [x] APInex JSON-вызовы и ошибки разбора журналируются в SQLite без системного промпта и image base64; срок хранения — 30 дней - [x] `/clear` полностью очищает данные текущего пользователя: ToV, скриншоты, историю постов, историю идей и настройки; API-логи сохраняются - [x] `/idea` возвращает одну идею одним сообщением и сохраняет её для исключения повторов -- [x] `/idea_voice` использует APInex TTS (`APINEX_TTS_MODEL`, default `aura-2-thalia-en`) и отправляет MP3 voice message; в API-логах только метаданные, соответствие русского голоса проверяется отдельно - [x] Все отправленные варианты постов сохраняются по пользователю, включая исправленные; история используется для новых идей - [x] `/clear` очищает историю пользовательских данных и оставляет API-логи - [x] Служебные ответы по-русски, язык текстов постов определяется запросом пользователя diff --git a/README.md b/README.md index 581bbd8..b77ae8e 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,10 @@ 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`, `APINEX_TTS_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 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` озвучивает одну идею через 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`. diff --git a/VERSION b/VERSION index f5f419b..9563e48 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0+16 +0.1.0+17 diff --git a/apinex_client.py b/apinex_client.py index 05883b0..1ef1e22 100644 --- a/apinex_client.py +++ b/apinex_client.py @@ -14,8 +14,6 @@ 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)" POSTS_SYSTEM_PROMPT = """ВСЕ служебные сообщения пользователю — на русском языке. Исключение — дословно приводимый технический текст ошибки. Сам текст публикации пиши на языке, который пользователь явно запросил; при отсутствии явного языкового требования используй язык исходного запроса. @@ -411,61 +409,6 @@ def _trim_history_posts(posts: list[str], char_limit: int = POST_HISTORY_CHAR_LI return result -async def synthesize_idea_voice(text: str, user_id: int | None = None) -> bytes: - 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, - ) - 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( request: str, tone_of_voice: str, history: list[str], idea_history: list[dict[str, str]], user_id: int | None = None ) -> dict[str, str]: diff --git a/bot.py b/bot.py index 345c85b..446f80b 100644 --- a/bot.py +++ b/bot.py @@ -12,9 +12,9 @@ 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 BufferedInputFile, Message +from aiogram.types import Message -from apinex_client import APINEX_STT_URL, POST_HISTORY_CHAR_LIMIT, POST_HISTORY_LIMIT, classify_intent, create_tone_profile, extract_posts_from_images, extract_transcript, generate_ideas, generate_posts, synthesize_idea_voice, transcribe_audio +from apinex_client import APINEX_STT_URL, POST_HISTORY_CHAR_LIMIT, POST_HISTORY_LIMIT, classify_intent, create_tone_profile, extract_posts_from_images, extract_transcript, generate_ideas, generate_posts, transcribe_audio from help_message import HELP_RICH_HTML, rich_to_plain from user_store import ( add_post_samples, @@ -164,13 +164,6 @@ async def help_command(message: Message) -> None: await message.answer(rich_to_plain(HELP_RICH_HTML)) -async def _send_idea_voice(message: Message, idea: dict[str, str], user_id: int) -> None: - text = f"{idea['title']}. {idea['angle']}. Зачем: {idea['why']}" - audio = await synthesize_idea_voice(text, user_id=user_id) - await message.answer_voice(BufferedInputFile(audio, filename="idea.mp3")) - await asyncio.to_thread(save_idea, user_id, idea) - - async def _send_idea(message: Message, idea: dict[str, str], user_id: int) -> None: await message.answer(f"{idea['title']}\n\n{idea['angle']}\n\nЗачем: {idea['why']}") await asyncio.to_thread(save_idea, user_id, idea) @@ -404,21 +397,6 @@ async def transcribe_voice(message: Message, bot: Bot) -> None: await report_error(message, "обработка голосового и генерация", exc) -async def idea_voice_command(message: Message) -> None: - if message.from_user is None: - await message.answer("Не удалось определить пользователя.") - return - try: - async with typing_status(message.bot, message.chat.id): - tone = await asyncio.to_thread(get_tone_of_voice, message.from_user.id) - history = await asyncio.to_thread(get_recent_post_history_text, message.from_user.id, POST_HISTORY_LIMIT, POST_HISTORY_CHAR_LIMIT) - idea_history = await asyncio.to_thread(get_idea_history, message.from_user.id, 30) - idea = await generate_ideas("предложи идею для поста", tone, history, idea_history, user_id=message.from_user.id) - await _send_idea_voice(message, idea, message.from_user.id) - except Exception as exc: - await report_error(message, "озвучка идеи", exc) - - @router.message(F.text & F.text.startswith("/")) async def handle_command_text(message: Message) -> None: command = (message.text or "").strip().split(maxsplit=1)[0].split("@", 1)[0].casefold() @@ -430,8 +408,6 @@ async def handle_command_text(message: Message) -> None: await clear_command(message) elif command == "/idea": await _classify_and_respond(message, "предложи идею для нового поста") - elif command == "/idea_voice": - await idea_voice_command(message) else: await message.answer("Неизвестная команда. Используй /help.") diff --git a/help_message.py b/help_message.py index adf2beb..0595c94 100644 --- a/help_message.py +++ b/help_message.py @@ -16,7 +16,6 @@ HELP_RICH_HTML = """

Помощь по боту

Попросить правку

Ответь реплаем на сообщение с вариантом и укажи, что изменить. Например: «Сделай короче и добавь призыв». Я учту оригинальный текст и верну три новых варианта отдельными сообщениями.

Команды

-

Команда /idea_voice отправляет одну идею голосовым сообщением.

Быстрые slash-команды только на английском: /help — эта инструкция; /start — краткое описание бота; /clear — полностью обнулить твои данные бота, кроме технических API-логов; /idea — предложить одну идею для нового поста. Можно попросить идею обычными словами: «предложи идею для поста», «о чём написать?», «накидай тему».

""" diff --git a/tests/test_release_help.py b/tests/test_release_help.py index 3be1537..a3b17f9 100644 --- a/tests/test_release_help.py +++ b/tests/test_release_help.py @@ -49,7 +49,7 @@ class HelpRoutingTests(unittest.TestCase): self.assertIn("elif command == \"/start\"", source) self.assertIn("elif command == \"/clear\"", source) self.assertIn("elif command == \"/idea\"", source) - self.assertIn("elif command == \"/idea_voice\"", source) + self.assertNotIn("idea_voice", source) def test_help_rich_message_contains_usage_scenarios(self): from help_message import HELP_RICH_HTML, rich_to_plain @@ -57,7 +57,7 @@ class HelpRoutingTests(unittest.TestCase): text = rich_to_plain(HELP_RICH_HTML) self.assertIn("/clear", text) self.assertIn("/idea", text) - self.assertIn("/idea_voice", text) + self.assertNotIn("/idea_voice", text) for term in ("пост", "голосовое", "скриншот", "реплаем", "/help"): self.assertIn(term, text) diff --git a/tests/test_writer_features.py b/tests/test_writer_features.py index d383fc7..dd00212 100644 --- a/tests/test_writer_features.py +++ b/tests/test_writer_features.py @@ -10,9 +10,10 @@ import user_store class FakeResponse: - def __init__(self, status, payload): + def __init__(self, status, payload, headers=None): self.status = status self.payload = payload + self.headers = headers or {} async def __aenter__(self): return self @@ -23,6 +24,12 @@ class FakeResponse: async def json(self, content_type=None): return self.payload + async def text(self): + if isinstance(self.payload, bytes): + return self.payload.decode("utf-8", errors="replace") + return str(self.payload) + + class FakeClientSession: def __init__(self, response, **kwargs): @@ -195,38 +202,6 @@ class ApinexTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(captured["system_prompt"], apinex_client.TONE_PROFILE_SYSTEM_PROMPT) self.assertEqual(tone, "Разговорный стиль, короткие абзацы") - async def test_idea_voice_sends_one_audio_message_and_persists_idea_after_send(self): - message = SimpleNamespace(answer_voice=AsyncMock()) - idea = {"title": "Заголовок", "angle": "Угол", "why": "Причина"} - - async def run_in_thread(function, *args): - return function(*args) - - with ( - patch.object(bot, "synthesize_idea_voice", new=AsyncMock(return_value=b"OggS-audio")), - patch.object(bot, "save_idea") as save_idea, - patch.object(bot.asyncio, "to_thread", side_effect=run_in_thread), - ): - await bot._send_idea_voice(message, idea, 91) - - message.answer_voice.assert_awaited_once() - voice_file = message.answer_voice.await_args.args[0] - self.assertEqual(voice_file.filename, "idea.mp3") - save_idea.assert_called_once_with(91, idea) - - async def test_synthesize_idea_voice_returns_mp3_audio(self): - def fake_run(command, **kwargs): - output_path = command[-1] if command[0].endswith("lame") else command[command.index("-w") + 1] - with open(output_path, "wb") as output_file: - output_file.write(b"ID3" + b"x" * 1200) - - with ( - patch.object(apinex_client.shutil, "which", side_effect=["/usr/bin/espeak-ng", "/usr/bin/lame"]), - patch.object(apinex_client.subprocess, "run", side_effect=fake_run), - ): - audio = await apinex_client.synthesize_idea_voice("Идея для поста о кофе") - self.assertTrue(audio.startswith(b"ID3")) - self.assertGreater(len(audio), 1000) async def test_idea_generation_returns_topics_only_and_uses_history(self): captured = {}