feat: add FSM handlers and admin interface
- Add all user FSM states (INTRO through CONFIRM_SAVE) - Add replica re-recording by number (ASK_REPLICA_NUMBER, REPEAT_REPLICA) - Add admin interface with stats and scenario upload - Add voice message handling and storage
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Телеграм бот для сбора датасета для автоматического протоколирования совещаний
|
# Телеграм бот для сбора датасета для автоматического протоколирования совещаний
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Создать файл .env из .env.example
|
||||||
|
cp .env.example .env
|
||||||
|
uv run main.py
|
||||||
|
```
|
||||||
|
|
||||||
## Данные
|
## Данные
|
||||||
|
|
||||||
### Формат входных данных
|
### Формат входных данных
|
||||||
|
|||||||
70
main.py
70
main.py
@@ -1,7 +1,32 @@
|
|||||||
from telegram.ext import ApplicationBuilder
|
from telegram.ext import (
|
||||||
|
ApplicationBuilder,
|
||||||
|
CallbackQueryHandler,
|
||||||
|
CommandHandler,
|
||||||
|
MessageHandler,
|
||||||
|
filters,
|
||||||
|
)
|
||||||
|
|
||||||
from src.config import BOT_TOKEN
|
from src.config import BOT_TOKEN
|
||||||
from src.database import init_db
|
from src.database import init_db
|
||||||
|
from src.handlers import (
|
||||||
|
admin_command,
|
||||||
|
handle_accept_intro,
|
||||||
|
handle_admin_document,
|
||||||
|
handle_ask_replica_number,
|
||||||
|
handle_cancel_ask_number,
|
||||||
|
handle_cancel_restart,
|
||||||
|
handle_cancel_upload,
|
||||||
|
handle_confirm_restart,
|
||||||
|
handle_confirm_upload,
|
||||||
|
handle_exit_admin,
|
||||||
|
handle_replica_number_input,
|
||||||
|
handle_rerecord_last,
|
||||||
|
handle_rerecord_previous,
|
||||||
|
handle_restart_track,
|
||||||
|
handle_save_track,
|
||||||
|
handle_voice_message,
|
||||||
|
start_command,
|
||||||
|
)
|
||||||
from src.logger import logger
|
from src.logger import logger
|
||||||
|
|
||||||
|
|
||||||
@@ -13,7 +38,48 @@ def main() -> None:
|
|||||||
|
|
||||||
app = ApplicationBuilder().token(BOT_TOKEN).build()
|
app = ApplicationBuilder().token(BOT_TOKEN).build()
|
||||||
|
|
||||||
# TODO: добавить обработчики
|
# Команды
|
||||||
|
app.add_handler(CommandHandler("start", start_command))
|
||||||
|
app.add_handler(CommandHandler("admin", admin_command))
|
||||||
|
|
||||||
|
# Callback query handlers
|
||||||
|
app.add_handler(CallbackQueryHandler(handle_accept_intro, pattern="^accept_intro$"))
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_rerecord_previous, pattern="^rerecord_previous$")
|
||||||
|
)
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_restart_track, pattern="^restart_track$")
|
||||||
|
)
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_confirm_restart, pattern="^confirm_restart$")
|
||||||
|
)
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_cancel_restart, pattern="^cancel_restart$")
|
||||||
|
)
|
||||||
|
app.add_handler(CallbackQueryHandler(handle_save_track, pattern="^save_track$"))
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_rerecord_last, pattern="^rerecord_last$")
|
||||||
|
)
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_ask_replica_number, pattern="^ask_replica_number$")
|
||||||
|
)
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_cancel_ask_number, pattern="^cancel_ask_number$")
|
||||||
|
)
|
||||||
|
app.add_handler(CallbackQueryHandler(handle_exit_admin, pattern="^exit_admin$"))
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_confirm_upload, pattern="^confirm_upload$")
|
||||||
|
)
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_cancel_upload, pattern="^cancel_upload$")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Message handlers
|
||||||
|
app.add_handler(MessageHandler(filters.VOICE, handle_voice_message))
|
||||||
|
app.add_handler(
|
||||||
|
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_replica_number_input)
|
||||||
|
)
|
||||||
|
app.add_handler(MessageHandler(filters.Document.ALL, handle_admin_document))
|
||||||
|
|
||||||
logger.info("Бот запущен")
|
logger.info("Бот запущен")
|
||||||
app.run_polling()
|
app.run_polling()
|
||||||
|
|||||||
31
src/audio.py
Normal file
31
src/audio.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from telegram import Bot
|
||||||
|
|
||||||
|
from src.config import DATA_PARTIAL_DIR
|
||||||
|
from src.database import upsert_recording
|
||||||
|
from src.logger import logger
|
||||||
|
from src.scenarios import get_audio_filename
|
||||||
|
|
||||||
|
|
||||||
|
async def save_voice_message(
|
||||||
|
bot: Bot,
|
||||||
|
file_id: str,
|
||||||
|
user_id: int,
|
||||||
|
scenario_id: str,
|
||||||
|
replica_index: int,
|
||||||
|
) -> None:
|
||||||
|
"""Сохраняет голосовое сообщение в data_partial/."""
|
||||||
|
# Создаём директорию если нужно
|
||||||
|
scenario_dir = DATA_PARTIAL_DIR / scenario_id
|
||||||
|
scenario_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Скачиваем файл
|
||||||
|
file = await bot.get_file(file_id)
|
||||||
|
filename = get_audio_filename(replica_index, user_id)
|
||||||
|
filepath = scenario_dir / filename
|
||||||
|
|
||||||
|
await file.download_to_drive(filepath)
|
||||||
|
|
||||||
|
# Записываем в БД
|
||||||
|
upsert_recording(user_id, scenario_id, replica_index)
|
||||||
|
|
||||||
|
logger.debug(f"Saved voice: {filepath}")
|
||||||
@@ -164,7 +164,11 @@ def get_or_create_user(telegram_id: int) -> User:
|
|||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
|
|
||||||
if row:
|
if row:
|
||||||
return User(id=row["id"], telegram_id=row["telegram_id"], created_at=row["created_at"])
|
return User(
|
||||||
|
id=row["id"],
|
||||||
|
telegram_id=row["telegram_id"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
)
|
||||||
|
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"INSERT INTO users (telegram_id) VALUES (?) RETURNING id, telegram_id, created_at",
|
"INSERT INTO users (telegram_id) VALUES (?) RETURNING id, telegram_id, created_at",
|
||||||
@@ -173,7 +177,9 @@ def get_or_create_user(telegram_id: int) -> User:
|
|||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info(f"Создан новый пользователь: dataset_speaker_id={row['id']}")
|
logger.info(f"Создан новый пользователь: dataset_speaker_id={row['id']}")
|
||||||
return User(id=row["id"], telegram_id=row["telegram_id"], created_at=row["created_at"])
|
return User(
|
||||||
|
id=row["id"], telegram_id=row["telegram_id"], created_at=row["created_at"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_telegram_id(telegram_id: int) -> User | None:
|
def get_user_by_telegram_id(telegram_id: int) -> User | None:
|
||||||
@@ -185,7 +191,11 @@ def get_user_by_telegram_id(telegram_id: int) -> User | None:
|
|||||||
)
|
)
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
return User(id=row["id"], telegram_id=row["telegram_id"], created_at=row["created_at"])
|
return User(
|
||||||
|
id=row["id"],
|
||||||
|
telegram_id=row["telegram_id"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -221,8 +231,13 @@ def get_scenario(scenario_id: str) -> Scenario | None:
|
|||||||
def get_all_scenarios() -> list[Scenario]:
|
def get_all_scenarios() -> list[Scenario]:
|
||||||
"""Получает все сценарии."""
|
"""Получает все сценарии."""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
cursor = conn.execute("SELECT id, created_at FROM scenarios ORDER BY created_at")
|
cursor = conn.execute(
|
||||||
return [Scenario(id=row["id"], created_at=row["created_at"]) for row in cursor.fetchall()]
|
"SELECT id, created_at FROM scenarios ORDER BY created_at"
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
Scenario(id=row["id"], created_at=row["created_at"])
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# === Replicas CRUD ===
|
# === Replicas CRUD ===
|
||||||
@@ -233,7 +248,10 @@ def create_replicas(scenario_id: str, replicas: list[tuple[int, int, str]]) -> N
|
|||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
conn.executemany(
|
conn.executemany(
|
||||||
"INSERT INTO replicas (scenario_id, speaker_id, replica_index, text) VALUES (?, ?, ?, ?)",
|
"INSERT INTO replicas (scenario_id, speaker_id, replica_index, text) VALUES (?, ?, ?, ?)",
|
||||||
[(scenario_id, speaker_id, idx, text) for speaker_id, idx, text in replicas],
|
[
|
||||||
|
(scenario_id, speaker_id, idx, text)
|
||||||
|
for speaker_id, idx, text in replicas
|
||||||
|
],
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -383,7 +401,9 @@ def get_user_session(user_id: int) -> UserSession | None:
|
|||||||
scenario_id=row["scenario_id"],
|
scenario_id=row["scenario_id"],
|
||||||
speaker_id=row["speaker_id"],
|
speaker_id=row["speaker_id"],
|
||||||
replica_index=row["replica_index"],
|
replica_index=row["replica_index"],
|
||||||
previous_state=UserState(row["previous_state"]) if row["previous_state"] else None,
|
previous_state=UserState(row["previous_state"])
|
||||||
|
if row["previous_state"]
|
||||||
|
else None,
|
||||||
last_bot_message_id=row["last_bot_message_id"],
|
last_bot_message_id=row["last_bot_message_id"],
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
@@ -437,10 +457,14 @@ def get_stats() -> dict:
|
|||||||
stats = {}
|
stats = {}
|
||||||
|
|
||||||
# Общее количество сценариев
|
# Общее количество сценариев
|
||||||
stats["total_scenarios"] = conn.execute("SELECT COUNT(*) FROM scenarios").fetchone()[0]
|
stats["total_scenarios"] = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM scenarios"
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
# Общее количество реплик
|
# Общее количество реплик
|
||||||
stats["total_replicas"] = conn.execute("SELECT COUNT(*) FROM replicas").fetchone()[0]
|
stats["total_replicas"] = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM replicas"
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
# Общее количество дорожек
|
# Общее количество дорожек
|
||||||
stats["total_tracks"] = conn.execute(
|
stats["total_tracks"] = conn.execute(
|
||||||
@@ -451,7 +475,9 @@ def get_stats() -> dict:
|
|||||||
stats["total_users"] = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
stats["total_users"] = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
||||||
|
|
||||||
# Количество озвученных реплик
|
# Количество озвученных реплик
|
||||||
stats["total_recordings"] = conn.execute("SELECT COUNT(*) FROM recordings").fetchone()[0]
|
stats["total_recordings"] = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM recordings"
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
# Количество полностью озвученных дорожек (в data/)
|
# Количество полностью озвученных дорожек (в data/)
|
||||||
# Это вычисляется по файловой системе, здесь примерная оценка
|
# Это вычисляется по файловой системе, здесь примерная оценка
|
||||||
|
|||||||
845
src/handlers.py
Normal file
845
src/handlers.py
Normal file
@@ -0,0 +1,845 @@
|
|||||||
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from src.config import ADMIN_LOGIN
|
||||||
|
from src.database import (
|
||||||
|
UserSession,
|
||||||
|
UserState,
|
||||||
|
get_connection,
|
||||||
|
get_or_create_user,
|
||||||
|
get_replicas_for_track,
|
||||||
|
get_stats,
|
||||||
|
get_user_session,
|
||||||
|
get_users_in_state,
|
||||||
|
upsert_user_session,
|
||||||
|
)
|
||||||
|
from src.logger import logger
|
||||||
|
from src.scenarios import find_available_track
|
||||||
|
|
||||||
|
# === Тексты сообщений ===
|
||||||
|
|
||||||
|
INTRO_TEXT = """👋 Добро пожаловать!
|
||||||
|
|
||||||
|
Это бот для сбора датасета озвученных реплик совещаний.
|
||||||
|
|
||||||
|
Вы будете озвучивать реплики участников совещаний. Каждая дорожка — это реплики одного участника в рамках одного совещания.
|
||||||
|
|
||||||
|
📋 Отправляя голосовые сообщения, вы соглашаетесь с тем, что они будут использованы в исследовательских целях для обучения моделей машинного обучения.
|
||||||
|
|
||||||
|
Нажмите кнопку ниже, чтобы начать."""
|
||||||
|
|
||||||
|
NO_MORE_SCENARIOS_TEXT = """📭 Пока нет доступных сценариев для озвучивания.
|
||||||
|
|
||||||
|
Вы получите уведомление, когда появятся новые сценарии."""
|
||||||
|
|
||||||
|
FIRST_REPLICA_INSTRUCTIONS = """🎙 Начинаем запись дорожки!
|
||||||
|
|
||||||
|
Отправляйте голосовые сообщения с озвучкой реплик. Говорите чётко и естественно.
|
||||||
|
|
||||||
|
📝 Реплика 1:"""
|
||||||
|
|
||||||
|
SHOW_REPLICA_TEXT = "📝 Реплика {num}:"
|
||||||
|
|
||||||
|
CONFIRM_RESTART_TEXT = """⚠️ Вы уверены, что хотите начать заново?
|
||||||
|
|
||||||
|
Все текущие записи этой дорожки будут удалены.
|
||||||
|
|
||||||
|
💡 Подсказка: после завершения записи можно будет перезаписать отдельные реплики."""
|
||||||
|
|
||||||
|
CONFIRM_SAVE_TEXT = """✅ Дорожка полностью озвучена!
|
||||||
|
|
||||||
|
Сохранить результат?"""
|
||||||
|
|
||||||
|
INVALID_INPUT_TEXT = "❌ Пожалуйста, отправьте голосовое сообщение."
|
||||||
|
|
||||||
|
INVALID_STATE_TEXT = "❌ Некорректное действие. Используйте /start для начала."
|
||||||
|
|
||||||
|
|
||||||
|
# === Клавиатуры ===
|
||||||
|
|
||||||
|
|
||||||
|
def get_intro_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
"✅ Принять и продолжить", callback_data="accept_intro"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_show_replica_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
"🔄 Перезаписать предыдущую", callback_data="rerecord_previous"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[InlineKeyboardButton("🔁 Начать заново", callback_data="restart_track")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_confirm_restart_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
"✅ Да, начать заново", callback_data="confirm_restart"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
"❌ Нет, продолжить", callback_data="cancel_restart"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_confirm_save_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[InlineKeyboardButton("💾 Сохранить", callback_data="save_track")],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
"🔄 Перезаписать последнюю", callback_data="rerecord_last"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
"🔢 Перезаписать по номеру", callback_data="ask_replica_number"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# === Вспомогательные функции ===
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_previous_keyboard(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE, session: UserSession
|
||||||
|
) -> None:
|
||||||
|
"""Удаляет inline-кнопки с предыдущего сообщения."""
|
||||||
|
if session.last_bot_message_id:
|
||||||
|
try:
|
||||||
|
await context.bot.edit_message_reply_markup(
|
||||||
|
chat_id=update.effective_chat.id,
|
||||||
|
message_id=session.last_bot_message_id,
|
||||||
|
reply_markup=None,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass # Сообщение могло быть удалено
|
||||||
|
|
||||||
|
|
||||||
|
async def send_message_and_save(
|
||||||
|
update: Update,
|
||||||
|
context: ContextTypes.DEFAULT_TYPE,
|
||||||
|
session: UserSession,
|
||||||
|
text: str,
|
||||||
|
keyboard: InlineKeyboardMarkup | None = None,
|
||||||
|
) -> int:
|
||||||
|
"""Отправляет сообщение и сохраняет его ID в сессии."""
|
||||||
|
await remove_previous_keyboard(update, context, session)
|
||||||
|
msg = await update.effective_chat.send_message(text, reply_markup=keyboard)
|
||||||
|
return msg.message_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_replica_text(session: UserSession) -> str | None:
|
||||||
|
"""Возвращает текст текущей реплики."""
|
||||||
|
if (
|
||||||
|
not session.scenario_id
|
||||||
|
or session.speaker_id is None
|
||||||
|
or session.replica_index is None
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
replicas = get_replicas_for_track(session.scenario_id, session.speaker_id)
|
||||||
|
# Находим реплику по индексу в дорожке
|
||||||
|
track_replicas = [r for r in replicas]
|
||||||
|
if session.replica_index < len(track_replicas):
|
||||||
|
return track_replicas[session.replica_index].text
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_track_length(scenario_id: str, speaker_id: int) -> int:
|
||||||
|
"""Возвращает количество реплик в дорожке."""
|
||||||
|
return len(get_replicas_for_track(scenario_id, speaker_id))
|
||||||
|
|
||||||
|
|
||||||
|
# === Обработчики состояний ===
|
||||||
|
|
||||||
|
|
||||||
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Обработчик команды /start."""
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session:
|
||||||
|
session = UserSession(
|
||||||
|
user_id=user.id,
|
||||||
|
state=UserState.INTRO,
|
||||||
|
scenario_id=None,
|
||||||
|
speaker_id=None,
|
||||||
|
replica_index=None,
|
||||||
|
previous_state=None,
|
||||||
|
last_bot_message_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.state = UserState.INTRO
|
||||||
|
msg_id = await send_message_and_save(
|
||||||
|
update, context, session, INTRO_TEXT, get_intro_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = msg_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
logger.info(f"User {user.id} started bot")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_accept_intro(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик кнопки 'Принять и продолжить'."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.INTRO:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ищем доступную дорожку
|
||||||
|
track = find_available_track(user.id)
|
||||||
|
|
||||||
|
if not track:
|
||||||
|
session.state = UserState.NO_MORE_SCENARIOS
|
||||||
|
await query.edit_message_text(NO_MORE_SCENARIOS_TEXT)
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Начинаем запись дорожки
|
||||||
|
scenario_id, speaker_id = track
|
||||||
|
session.state = UserState.FIRST_REPLICA
|
||||||
|
session.scenario_id = scenario_id
|
||||||
|
session.speaker_id = speaker_id
|
||||||
|
session.replica_index = 0
|
||||||
|
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}"
|
||||||
|
|
||||||
|
await query.edit_message_text(text)
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
logger.info(f"User {user.id} started track {scenario_id}/{speaker_id}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_voice_message(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик голосовых сообщений."""
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session:
|
||||||
|
await update.message.reply_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
valid_states = {
|
||||||
|
UserState.FIRST_REPLICA,
|
||||||
|
UserState.SHOW_REPLICA,
|
||||||
|
UserState.REPEAT_REPLICA,
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.state not in valid_states:
|
||||||
|
await update.message.reply_text(INVALID_INPUT_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Импортируем здесь, чтобы избежать циклических импортов
|
||||||
|
from src.audio import save_voice_message
|
||||||
|
|
||||||
|
# Сохраняем голосовое сообщение
|
||||||
|
voice = update.message.voice
|
||||||
|
await save_voice_message(
|
||||||
|
context.bot,
|
||||||
|
voice.file_id,
|
||||||
|
user.id,
|
||||||
|
session.scenario_id,
|
||||||
|
session.replica_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
track_length = get_track_length(session.scenario_id, session.speaker_id)
|
||||||
|
|
||||||
|
# Обработка REPEAT_REPLICA — возврат в CONFIRM_SAVE
|
||||||
|
if session.state == UserState.REPEAT_REPLICA:
|
||||||
|
session.state = UserState.CONFIRM_SAVE
|
||||||
|
msg_id = await send_message_and_save(
|
||||||
|
update, context, session, CONFIRM_SAVE_TEXT, get_confirm_save_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = msg_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Переход к следующей реплике
|
||||||
|
session.replica_index += 1
|
||||||
|
|
||||||
|
if session.replica_index >= track_length:
|
||||||
|
# Дорожка завершена
|
||||||
|
session.state = UserState.CONFIRM_SAVE
|
||||||
|
msg_id = await send_message_and_save(
|
||||||
|
update, context, session, CONFIRM_SAVE_TEXT, get_confirm_save_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = msg_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Показываем следующую реплику
|
||||||
|
session.state = UserState.SHOW_REPLICA
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = (
|
||||||
|
f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
msg_id = await send_message_and_save(
|
||||||
|
update, context, session, text, get_show_replica_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = msg_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_rerecord_previous(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик кнопки 'Перезаписать предыдущую'."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.SHOW_REPLICA:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
session.replica_index -= 1
|
||||||
|
|
||||||
|
if session.replica_index == 0:
|
||||||
|
session.state = UserState.FIRST_REPLICA
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}"
|
||||||
|
await query.edit_message_text(text)
|
||||||
|
else:
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
|
||||||
|
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
|
||||||
|
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_restart_track(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик кнопки 'Начать заново'."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.SHOW_REPLICA:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
session.state = UserState.CONFIRM_RESTART
|
||||||
|
await query.edit_message_text(
|
||||||
|
CONFIRM_RESTART_TEXT, reply_markup=get_confirm_restart_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_confirm_restart(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик подтверждения рестарта дорожки."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.CONFIRM_RESTART:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Удаляем частичные записи
|
||||||
|
from src.database import delete_user_recordings_for_scenario
|
||||||
|
from src.scenarios import delete_partial_track
|
||||||
|
|
||||||
|
delete_partial_track(user.id, session.scenario_id, session.speaker_id)
|
||||||
|
delete_user_recordings_for_scenario(user.id, session.scenario_id)
|
||||||
|
|
||||||
|
# Начинаем заново
|
||||||
|
session.state = UserState.FIRST_REPLICA
|
||||||
|
session.replica_index = 0
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}"
|
||||||
|
|
||||||
|
await query.edit_message_text(text)
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
logger.info(
|
||||||
|
f"User {user.id} restarted track {session.scenario_id}/{session.speaker_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_cancel_restart(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик отмены рестарта."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.CONFIRM_RESTART:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
session.state = UserState.SHOW_REPLICA
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = (
|
||||||
|
f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_save_track(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Обработчик сохранения дорожки."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.CONFIRM_SAVE:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
from src.scenarios import move_track_to_data
|
||||||
|
|
||||||
|
# Переносим файлы в data/
|
||||||
|
move_track_to_data(user.id, session.scenario_id, session.speaker_id)
|
||||||
|
|
||||||
|
# Ищем следующую дорожку
|
||||||
|
track = find_available_track(user.id)
|
||||||
|
|
||||||
|
if not track:
|
||||||
|
session.state = UserState.NO_MORE_SCENARIOS
|
||||||
|
session.scenario_id = None
|
||||||
|
session.speaker_id = None
|
||||||
|
session.replica_index = None
|
||||||
|
await query.edit_message_text(
|
||||||
|
"✅ Дорожка сохранена!\n\n" + NO_MORE_SCENARIOS_TEXT
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Начинаем новую дорожку
|
||||||
|
scenario_id, speaker_id = track
|
||||||
|
session.state = UserState.FIRST_REPLICA
|
||||||
|
session.scenario_id = scenario_id
|
||||||
|
session.speaker_id = speaker_id
|
||||||
|
session.replica_index = 0
|
||||||
|
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = f"✅ Дорожка сохранена!\n\n{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}"
|
||||||
|
|
||||||
|
await query.edit_message_text(text)
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
logger.info(f"User {user.id} saved track, started new: {scenario_id}/{speaker_id}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_rerecord_last(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик 'Перезаписать последнюю реплику'."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.CONFIRM_SAVE:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
track_length = get_track_length(session.scenario_id, session.speaker_id)
|
||||||
|
session.state = UserState.SHOW_REPLICA
|
||||||
|
session.replica_index = track_length - 1
|
||||||
|
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = (
|
||||||
|
f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
# === Этап 5: Перезапись по номеру ===
|
||||||
|
|
||||||
|
ASK_REPLICA_NUMBER_TEXT = "🔢 Введите номер реплики для перезаписи (1-{max}):"
|
||||||
|
|
||||||
|
REPEAT_REPLICA_TEXT = "🔄 Перезапись реплики {num}:"
|
||||||
|
|
||||||
|
|
||||||
|
def get_ask_replica_number_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
[[InlineKeyboardButton("❌ Отмена", callback_data="cancel_ask_number")]]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_ask_replica_number(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик кнопки 'Перезаписать по номеру'."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.CONFIRM_SAVE:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
track_length = get_track_length(session.scenario_id, session.speaker_id)
|
||||||
|
session.state = UserState.ASK_REPLICA_NUMBER
|
||||||
|
|
||||||
|
text = ASK_REPLICA_NUMBER_TEXT.format(max=track_length)
|
||||||
|
await query.edit_message_text(text, reply_markup=get_ask_replica_number_keyboard())
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_cancel_ask_number(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик отмены ввода номера реплики."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.ASK_REPLICA_NUMBER:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
session.state = UserState.CONFIRM_SAVE
|
||||||
|
await query.edit_message_text(
|
||||||
|
CONFIRM_SAVE_TEXT, reply_markup=get_confirm_save_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_replica_number_input(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик ввода номера реплики."""
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.ASK_REPLICA_NUMBER:
|
||||||
|
return
|
||||||
|
|
||||||
|
text = update.message.text.strip()
|
||||||
|
track_length = get_track_length(session.scenario_id, session.speaker_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
num = int(text)
|
||||||
|
if num < 1 or num > track_length:
|
||||||
|
raise ValueError()
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text(f"❌ Введите число от 1 до {track_length}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Переход в REPEAT_REPLICA
|
||||||
|
session.state = UserState.REPEAT_REPLICA
|
||||||
|
session.replica_index = num - 1 # 0-indexed
|
||||||
|
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = f"{REPEAT_REPLICA_TEXT.format(num=num)}\n\n{replica_text}"
|
||||||
|
|
||||||
|
msg_id = await send_message_and_save(update, context, session, text)
|
||||||
|
session.last_bot_message_id = msg_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
# === Этап 7: Административный интерфейс ===
|
||||||
|
|
||||||
|
|
||||||
|
def get_admin_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
"👤 Вернуться в пользовательский режим", callback_data="exit_admin"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_admin_upload_confirm_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[InlineKeyboardButton("✅ Да, добавить", callback_data="confirm_upload")],
|
||||||
|
[InlineKeyboardButton("❌ Отмена", callback_data="cancel_upload")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_admin_stats() -> str:
|
||||||
|
"""Форматирует статистику для админки."""
|
||||||
|
stats = get_stats()
|
||||||
|
return f"""📊 Статистика датасета:
|
||||||
|
|
||||||
|
📁 Сценарии: {stats["total_scenarios"]}
|
||||||
|
🎵 Дорожки: {stats["total_tracks"]} (завершено: {stats["completed_tracks"]})
|
||||||
|
💬 Реплики: {stats["total_replicas"]}
|
||||||
|
🎙 Записей: {stats["total_recordings"]}
|
||||||
|
👥 Пользователей: {stats["total_users"]}
|
||||||
|
|
||||||
|
Отправьте JSON-файл с новым сценарием для загрузки."""
|
||||||
|
|
||||||
|
|
||||||
|
async def admin_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Обработчик команды /admin."""
|
||||||
|
username = update.effective_user.username
|
||||||
|
|
||||||
|
if username != ADMIN_LOGIN:
|
||||||
|
await update.message.reply_text("❌ У вас нет прав администратора.")
|
||||||
|
return
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session:
|
||||||
|
session = UserSession(
|
||||||
|
user_id=user.id,
|
||||||
|
state=UserState.ADMIN,
|
||||||
|
scenario_id=None,
|
||||||
|
speaker_id=None,
|
||||||
|
replica_index=None,
|
||||||
|
previous_state=None,
|
||||||
|
last_bot_message_id=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
session.previous_state = session.state
|
||||||
|
session.state = UserState.ADMIN
|
||||||
|
|
||||||
|
text = format_admin_stats()
|
||||||
|
msg_id = await send_message_and_save(
|
||||||
|
update, context, session, text, get_admin_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = msg_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
logger.info(f"Admin {username} entered admin mode")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_exit_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Обработчик выхода из админки."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.ADMIN:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Возвращаемся в предыдущее состояние или в INTRO
|
||||||
|
if session.previous_state:
|
||||||
|
session.state = session.previous_state
|
||||||
|
session.previous_state = None
|
||||||
|
else:
|
||||||
|
session.state = UserState.INTRO
|
||||||
|
|
||||||
|
# Показываем соответствующее сообщение
|
||||||
|
if session.state == UserState.INTRO:
|
||||||
|
await query.edit_message_text(INTRO_TEXT, reply_markup=get_intro_keyboard())
|
||||||
|
elif session.state == UserState.NO_MORE_SCENARIOS:
|
||||||
|
await query.edit_message_text(NO_MORE_SCENARIOS_TEXT)
|
||||||
|
elif session.state == UserState.FIRST_REPLICA:
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = f"{FIRST_REPLICA_INSTRUCTIONS}\n\n{replica_text}"
|
||||||
|
await query.edit_message_text(text)
|
||||||
|
elif session.state == UserState.SHOW_REPLICA:
|
||||||
|
replica_text = get_current_replica_text(session)
|
||||||
|
text = f"{SHOW_REPLICA_TEXT.format(num=session.replica_index + 1)}\n\n{replica_text}"
|
||||||
|
await query.edit_message_text(text, reply_markup=get_show_replica_keyboard())
|
||||||
|
elif session.state == UserState.CONFIRM_SAVE:
|
||||||
|
await query.edit_message_text(
|
||||||
|
CONFIRM_SAVE_TEXT, reply_markup=get_confirm_save_keyboard()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await query.edit_message_text("👤 Вы вернулись в пользовательский режим.")
|
||||||
|
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_admin_document(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик загрузки JSON файла в админке."""
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.ADMIN:
|
||||||
|
return
|
||||||
|
|
||||||
|
document = update.message.document
|
||||||
|
if not document.file_name.endswith(".json"):
|
||||||
|
await update.message.reply_text("❌ Ожидается JSON-файл.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Скачиваем и парсим файл
|
||||||
|
file = await context.bot.get_file(document.file_id)
|
||||||
|
file_bytes = await file.download_as_bytearray()
|
||||||
|
|
||||||
|
from src.scenarios import get_scenario_info, parse_scenario_file
|
||||||
|
|
||||||
|
json_data, error = parse_scenario_file(bytes(file_bytes))
|
||||||
|
if error:
|
||||||
|
await update.message.reply_text(f"❌ {error}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверяем, что сценарий с таким ID не существует
|
||||||
|
scenario_id = document.file_name.replace(".json", "")
|
||||||
|
from src.database import get_scenario
|
||||||
|
|
||||||
|
if get_scenario(scenario_id):
|
||||||
|
await update.message.reply_text(f"❌ Сценарий {scenario_id} уже существует.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Сохраняем данные для подтверждения
|
||||||
|
context.user_data["pending_scenario"] = {
|
||||||
|
"id": scenario_id,
|
||||||
|
"data": json_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
info = get_scenario_info(json_data)
|
||||||
|
text = f"""📄 Сценарий: {scenario_id}
|
||||||
|
|
||||||
|
📊 Информация:
|
||||||
|
- Реплик: {info["total_replicas"]}
|
||||||
|
- Дорожек: {info["total_tracks"]}
|
||||||
|
- Speaker IDs: {info["speaker_ids"]}
|
||||||
|
|
||||||
|
Добавить сценарий?"""
|
||||||
|
|
||||||
|
session.state = UserState.ADMIN_UPLOAD_CONFIRM
|
||||||
|
msg_id = await send_message_and_save(
|
||||||
|
update, context, session, text, get_admin_upload_confirm_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = msg_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_confirm_upload(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик подтверждения загрузки сценария."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.ADMIN_UPLOAD_CONFIRM:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
pending = context.user_data.get("pending_scenario")
|
||||||
|
if not pending:
|
||||||
|
await query.edit_message_text("❌ Данные сценария потеряны. Попробуйте снова.")
|
||||||
|
session.state = UserState.ADMIN
|
||||||
|
upsert_user_session(session)
|
||||||
|
return
|
||||||
|
|
||||||
|
from src.scenarios import load_scenario_from_json
|
||||||
|
|
||||||
|
try:
|
||||||
|
load_scenario_from_json(pending["id"], pending["data"])
|
||||||
|
except ValueError as e:
|
||||||
|
await query.edit_message_text(f"❌ Ошибка: {e}")
|
||||||
|
session.state = UserState.ADMIN
|
||||||
|
upsert_user_session(session)
|
||||||
|
return
|
||||||
|
|
||||||
|
del context.user_data["pending_scenario"]
|
||||||
|
|
||||||
|
# Уведомляем пользователей в NO_MORE_SCENARIOS
|
||||||
|
users_waiting = get_users_in_state(UserState.NO_MORE_SCENARIOS)
|
||||||
|
for waiting_user_id in users_waiting:
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT telegram_id FROM users WHERE id = ?", (waiting_user_id,)
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
await context.bot.send_message(
|
||||||
|
row[0],
|
||||||
|
"🎉 Появился новый сценарий для озвучивания! Используйте /start для продолжения.",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass # Не удалось отправить уведомление
|
||||||
|
|
||||||
|
session.state = UserState.ADMIN
|
||||||
|
text = f"✅ Сценарий {pending['id']} добавлен!\n\n" + format_admin_stats()
|
||||||
|
await query.edit_message_text(text, reply_markup=get_admin_keyboard())
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
logger.info(f"Scenario {pending['id']} uploaded by admin")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_cancel_upload(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик отмены загрузки сценария."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
user = get_or_create_user(update.effective_user.id)
|
||||||
|
session = get_user_session(user.id)
|
||||||
|
|
||||||
|
if not session or session.state != UserState.ADMIN_UPLOAD_CONFIRM:
|
||||||
|
await query.edit_message_text(INVALID_STATE_TEXT)
|
||||||
|
return
|
||||||
|
|
||||||
|
if "pending_scenario" in context.user_data:
|
||||||
|
del context.user_data["pending_scenario"]
|
||||||
|
|
||||||
|
session.state = UserState.ADMIN
|
||||||
|
text = format_admin_stats()
|
||||||
|
await query.edit_message_text(text, reply_markup=get_admin_keyboard())
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
Reference in New Issue
Block a user