Пол пользователя
This commit is contained in:
@@ -30,6 +30,7 @@ docker compose up -d --build
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"text": "text of the replica",
|
"text": "text of the replica",
|
||||||
|
"gender": "male",
|
||||||
"speaker_id": 0
|
"speaker_id": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -54,7 +55,8 @@ docker compose up -d --build
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
- **INTRO** - начальное состояние сессии сразу после команды `/start`. Выводится сообщение о том, что это за бот и небольшое пользовательское соглашение, уведомляющее о целях сбора данных. Единственная кнопка - `Принять и продолжить`. После нажатия на неё условный переход либо в **NO_MORE_SCENARIOS**, если нету доступных сценариев для озвучивания, либо в **FIRST_REPLICA** .
|
- **INTRO** - начальное состояние сессии сразу после команды `/start`. Выводится сообщение о том, что это за бот и небольшое пользовательское соглашение, уведомляющее о целях сбора данных. Единственная кнопка - `Принять и продолжить`. После нажатия на неё происходит переход в **SPECIFY_GENDER**.
|
||||||
|
- **SPECIFY_GENDER** - выводится сообщение с предложением указать пол пользователя. Две кнопки для выбора мужского и женского пола. После нажатия на любую из кнопок происходит условный переход либо в **NO_MORE_SCENARIOS**, если нету доступных сценариев для озвучивания, либо в **FIRST_REPLICA**.
|
||||||
- **NO_MORE_SCENARIOS** - выводится сообщение о том, что пока больше нет сценариев для озвучивания. Из **NO_MORE_SCENARIOS** может произойти переход в **FIRST_REPLICA**, когда на сервер загружается новый сценарий, при этом выводится дополнительное уведомление. Самостоятельно пользователь не может покинуть это состояние.
|
- **NO_MORE_SCENARIOS** - выводится сообщение о том, что пока больше нет сценариев для озвучивания. Из **NO_MORE_SCENARIOS** может произойти переход в **FIRST_REPLICA**, когда на сервер загружается новый сценарий, при этом выводится дополнительное уведомление. Самостоятельно пользователь не может покинуть это состояние.
|
||||||
- **FIRST_REPLICA** - выводится первая реплика дорожки с дополнительными инструкциями для пользователя (`i = 0`). При отправке аудиосообщения с озвучкой реплики, происходит условный переход в **SHOW_REPLICA** (`i += 1`), если это не последняя реплика в дорожке, либо в **CONFIRM_SAVE**.
|
- **FIRST_REPLICA** - выводится первая реплика дорожки с дополнительными инструкциями для пользователя (`i = 0`). При отправке аудиосообщения с озвучкой реплики, происходит условный переход в **SHOW_REPLICA** (`i += 1`), если это не последняя реплика в дорожке, либо в **CONFIRM_SAVE**.
|
||||||
- **SHOW_REPLICA** - выводится i-ая (0-indexed) реплика дорожки и её номер (1-indexed для пользователя, то есть i + 1). Есть две кнопки. Кнопка "Перезаписать предыдущую реплику", по ней условный переход в **FIRST_REPLICA**, если `i == 1`, либо в **SHOW_REPLICA** (`i -= 1`). Кнопка "Начать заново", по ней переход в **CONFIRM_RESTART**. При отправке аудиосообщения с озвучкой реплики, происходит условный переход в **SHOW_REPLICA** (`i += 1`), если это не последняя реплика в дорожке, либо в **CONFIRM_SAVE**.
|
- **SHOW_REPLICA** - выводится i-ая (0-indexed) реплика дорожки и её номер (1-indexed для пользователя, то есть i + 1). Есть две кнопки. Кнопка "Перезаписать предыдущую реплику", по ней условный переход в **FIRST_REPLICA**, если `i == 1`, либо в **SHOW_REPLICA** (`i -= 1`). Кнопка "Начать заново", по ней переход в **CONFIRM_RESTART**. При отправке аудиосообщения с озвучкой реплики, происходит условный переход в **SHOW_REPLICA** (`i += 1`), если это не последняя реплика в дорожке, либо в **CONFIRM_SAVE**.
|
||||||
|
|||||||
BIN
bot-states.png
BIN
bot-states.png
Binary file not shown.
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 219 KiB |
4
main.py
4
main.py
@@ -28,6 +28,7 @@ from src.handlers import (
|
|||||||
handle_rerecord_previous,
|
handle_rerecord_previous,
|
||||||
handle_restart_track,
|
handle_restart_track,
|
||||||
handle_save_track,
|
handle_save_track,
|
||||||
|
handle_select_gender,
|
||||||
handle_select_scenario_delete,
|
handle_select_scenario_delete,
|
||||||
handle_unexpected_text,
|
handle_unexpected_text,
|
||||||
handle_voice_message,
|
handle_voice_message,
|
||||||
@@ -50,6 +51,9 @@ def main() -> None:
|
|||||||
|
|
||||||
# Callback query handlers
|
# Callback query handlers
|
||||||
app.add_handler(CallbackQueryHandler(handle_accept_intro, pattern="^accept_intro$"))
|
app.add_handler(CallbackQueryHandler(handle_accept_intro, pattern="^accept_intro$"))
|
||||||
|
app.add_handler(
|
||||||
|
CallbackQueryHandler(handle_select_gender, pattern="^select_gender:")
|
||||||
|
)
|
||||||
app.add_handler(
|
app.add_handler(
|
||||||
CallbackQueryHandler(handle_rerecord_previous, pattern="^rerecord_previous$")
|
CallbackQueryHandler(handle_rerecord_previous, pattern="^rerecord_previous$")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class UserState(Enum):
|
|||||||
"""Состояния пользовательской сессии."""
|
"""Состояния пользовательской сессии."""
|
||||||
|
|
||||||
INTRO = "intro"
|
INTRO = "intro"
|
||||||
|
SPECIFY_GENDER = "specify_gender"
|
||||||
NO_MORE_SCENARIOS = "no_more_scenarios"
|
NO_MORE_SCENARIOS = "no_more_scenarios"
|
||||||
FIRST_REPLICA = "first_replica"
|
FIRST_REPLICA = "first_replica"
|
||||||
SHOW_REPLICA = "show_replica"
|
SHOW_REPLICA = "show_replica"
|
||||||
@@ -32,6 +33,7 @@ class User:
|
|||||||
id: int # dataset_speaker_id
|
id: int # dataset_speaker_id
|
||||||
telegram_id: int
|
telegram_id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
gender: str | None # "male" или "female"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -51,6 +53,7 @@ class Replica:
|
|||||||
speaker_id: int # в рамках сценария
|
speaker_id: int # в рамках сценария
|
||||||
replica_index: int # порядок в сценарии (0-indexed)
|
replica_index: int # порядок в сценарии (0-indexed)
|
||||||
text: str
|
text: str
|
||||||
|
gender: str # "male" или "female"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -101,7 +104,8 @@ def init_db() -> None:
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
telegram_id INTEGER UNIQUE NOT NULL,
|
telegram_id INTEGER UNIQUE NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
gender TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS scenarios (
|
CREATE TABLE IF NOT EXISTS scenarios (
|
||||||
@@ -115,6 +119,7 @@ def init_db() -> None:
|
|||||||
speaker_id INTEGER NOT NULL,
|
speaker_id INTEGER NOT NULL,
|
||||||
replica_index INTEGER NOT NULL,
|
replica_index INTEGER NOT NULL,
|
||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
|
gender TEXT NOT NULL,
|
||||||
FOREIGN KEY (scenario_id) REFERENCES scenarios(id),
|
FOREIGN KEY (scenario_id) REFERENCES scenarios(id),
|
||||||
UNIQUE(scenario_id, replica_index)
|
UNIQUE(scenario_id, replica_index)
|
||||||
);
|
);
|
||||||
@@ -152,14 +157,6 @@ def init_db() -> None:
|
|||||||
CREATE INDEX IF NOT EXISTS idx_recordings_scenario
|
CREATE INDEX IF NOT EXISTS idx_recordings_scenario
|
||||||
ON recordings(scenario_id);
|
ON recordings(scenario_id);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Миграция: добавляем колонку duration если её нет
|
|
||||||
cursor = conn.execute("PRAGMA table_info(recordings)")
|
|
||||||
columns = [row[1] for row in cursor.fetchall()]
|
|
||||||
if "duration" not in columns:
|
|
||||||
conn.execute("ALTER TABLE recordings ADD COLUMN duration REAL DEFAULT 0.0")
|
|
||||||
logger.info("Добавлена колонка duration в таблицу recordings")
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
logger.info("База данных инициализирована")
|
logger.info("База данных инициализирована")
|
||||||
@@ -172,7 +169,8 @@ def get_or_create_user(telegram_id: int) -> User:
|
|||||||
"""Получает или создаёт пользователя по telegram_id."""
|
"""Получает или создаёт пользователя по telegram_id."""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"SELECT id, telegram_id, created_at FROM users WHERE telegram_id = ?",
|
"SELECT id, telegram_id, created_at, gender FROM users "
|
||||||
|
"WHERE telegram_id = ?",
|
||||||
(telegram_id,),
|
(telegram_id,),
|
||||||
)
|
)
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
@@ -182,18 +180,22 @@ def get_or_create_user(telegram_id: int) -> User:
|
|||||||
id=row["id"],
|
id=row["id"],
|
||||||
telegram_id=row["telegram_id"],
|
telegram_id=row["telegram_id"],
|
||||||
created_at=row["created_at"],
|
created_at=row["created_at"],
|
||||||
|
gender=row["gender"],
|
||||||
)
|
)
|
||||||
|
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"INSERT INTO users (telegram_id) VALUES (?) "
|
"INSERT INTO users (telegram_id) VALUES (?) "
|
||||||
"RETURNING id, telegram_id, created_at",
|
"RETURNING id, telegram_id, created_at, gender",
|
||||||
(telegram_id,),
|
(telegram_id,),
|
||||||
)
|
)
|
||||||
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(
|
return User(
|
||||||
id=row["id"], telegram_id=row["telegram_id"], created_at=row["created_at"]
|
id=row["id"],
|
||||||
|
telegram_id=row["telegram_id"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
gender=row["gender"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -201,7 +203,8 @@ def get_user_by_telegram_id(telegram_id: int) -> User | None:
|
|||||||
"""Получает пользователя по telegram_id."""
|
"""Получает пользователя по telegram_id."""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"SELECT id, telegram_id, created_at FROM users WHERE telegram_id = ?",
|
"SELECT id, telegram_id, created_at, gender FROM users "
|
||||||
|
"WHERE telegram_id = ?",
|
||||||
(telegram_id,),
|
(telegram_id,),
|
||||||
)
|
)
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
@@ -210,10 +213,22 @@ def get_user_by_telegram_id(telegram_id: int) -> User | None:
|
|||||||
id=row["id"],
|
id=row["id"],
|
||||||
telegram_id=row["telegram_id"],
|
telegram_id=row["telegram_id"],
|
||||||
created_at=row["created_at"],
|
created_at=row["created_at"],
|
||||||
|
gender=row["gender"],
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_gender(user_id: int, gender: str) -> None:
|
||||||
|
"""Обновляет пол пользователя."""
|
||||||
|
with get_connection() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE users SET gender = ? WHERE id = ?",
|
||||||
|
(gender, user_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
logger.info(f"Обновлён пол пользователя {user_id}: {gender}")
|
||||||
|
|
||||||
|
|
||||||
# === Scenarios CRUD ===
|
# === Scenarios CRUD ===
|
||||||
|
|
||||||
|
|
||||||
@@ -258,15 +273,18 @@ def get_all_scenarios() -> list[Scenario]:
|
|||||||
# === Replicas CRUD ===
|
# === Replicas CRUD ===
|
||||||
|
|
||||||
|
|
||||||
def create_replicas(scenario_id: str, replicas: list[tuple[int, int, str]]) -> None:
|
def create_replicas(
|
||||||
"""Создаёт реплики. replicas: [(speaker_id, replica_index, text), ...]"""
|
scenario_id: str, replicas: list[tuple[int, int, str, str]]
|
||||||
|
) -> None:
|
||||||
|
"""Создаёт реплики. replicas: [(speaker_id, replica_index, text, gender), ...]"""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
conn.executemany(
|
conn.executemany(
|
||||||
"INSERT INTO replicas (scenario_id, speaker_id, replica_index, text) "
|
"INSERT INTO replicas "
|
||||||
"VALUES (?, ?, ?, ?)",
|
"(scenario_id, speaker_id, replica_index, text, gender) "
|
||||||
|
"VALUES (?, ?, ?, ?, ?)",
|
||||||
[
|
[
|
||||||
(scenario_id, speaker_id, idx, text)
|
(scenario_id, speaker_id, idx, text, gender)
|
||||||
for speaker_id, idx, text in replicas
|
for speaker_id, idx, text, gender in replicas
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -276,8 +294,8 @@ def get_replicas_for_scenario(scenario_id: str) -> list[Replica]:
|
|||||||
"""Получает все реплики сценария."""
|
"""Получает все реплики сценария."""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"SELECT id, scenario_id, speaker_id, replica_index, text FROM replicas "
|
"SELECT id, scenario_id, speaker_id, replica_index, text, gender "
|
||||||
"WHERE scenario_id = ? ORDER BY replica_index",
|
"FROM replicas WHERE scenario_id = ? ORDER BY replica_index",
|
||||||
(scenario_id,),
|
(scenario_id,),
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
@@ -287,6 +305,7 @@ def get_replicas_for_scenario(scenario_id: str) -> list[Replica]:
|
|||||||
speaker_id=row["speaker_id"],
|
speaker_id=row["speaker_id"],
|
||||||
replica_index=row["replica_index"],
|
replica_index=row["replica_index"],
|
||||||
text=row["text"],
|
text=row["text"],
|
||||||
|
gender=row["gender"],
|
||||||
)
|
)
|
||||||
for row in cursor.fetchall()
|
for row in cursor.fetchall()
|
||||||
]
|
]
|
||||||
@@ -296,8 +315,9 @@ def get_replicas_for_track(scenario_id: str, speaker_id: int) -> list[Replica]:
|
|||||||
"""Получает реплики для конкретной дорожки (speaker_id в сценарии)."""
|
"""Получает реплики для конкретной дорожки (speaker_id в сценарии)."""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"SELECT id, scenario_id, speaker_id, replica_index, text FROM replicas "
|
"SELECT id, scenario_id, speaker_id, replica_index, text, gender "
|
||||||
"WHERE scenario_id = ? AND speaker_id = ? ORDER BY replica_index",
|
"FROM replicas WHERE scenario_id = ? AND speaker_id = ? "
|
||||||
|
"ORDER BY replica_index",
|
||||||
(scenario_id, speaker_id),
|
(scenario_id, speaker_id),
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
@@ -307,6 +327,7 @@ def get_replicas_for_track(scenario_id: str, speaker_id: int) -> list[Replica]:
|
|||||||
speaker_id=row["speaker_id"],
|
speaker_id=row["speaker_id"],
|
||||||
replica_index=row["replica_index"],
|
replica_index=row["replica_index"],
|
||||||
text=row["text"],
|
text=row["text"],
|
||||||
|
gender=row["gender"],
|
||||||
)
|
)
|
||||||
for row in cursor.fetchall()
|
for row in cursor.fetchall()
|
||||||
]
|
]
|
||||||
@@ -323,6 +344,16 @@ def get_track_speaker_ids(scenario_id: str) -> list[int]:
|
|||||||
return [row["speaker_id"] for row in cursor.fetchall()]
|
return [row["speaker_id"] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def get_scenario_genders(scenario_id: str) -> set[str]:
|
||||||
|
"""Получает список полов, представленных в сценарии."""
|
||||||
|
with get_connection() as conn:
|
||||||
|
cursor = conn.execute(
|
||||||
|
"SELECT DISTINCT gender FROM replicas WHERE scenario_id = ?",
|
||||||
|
(scenario_id,),
|
||||||
|
)
|
||||||
|
return {row["gender"] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
|
||||||
# === Recordings CRUD ===
|
# === Recordings CRUD ===
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from src.database import (
|
|||||||
get_or_create_user,
|
get_or_create_user,
|
||||||
get_replicas_for_track,
|
get_replicas_for_track,
|
||||||
get_scenario,
|
get_scenario,
|
||||||
|
get_scenario_genders,
|
||||||
get_scenario_stats,
|
get_scenario_stats,
|
||||||
get_stats,
|
get_stats,
|
||||||
get_user_audio_duration,
|
get_user_audio_duration,
|
||||||
@@ -20,6 +21,7 @@ from src.database import (
|
|||||||
get_user_stats,
|
get_user_stats,
|
||||||
get_users_in_state,
|
get_users_in_state,
|
||||||
get_users_with_scenario,
|
get_users_with_scenario,
|
||||||
|
update_user_gender,
|
||||||
upsert_user_session,
|
upsert_user_session,
|
||||||
)
|
)
|
||||||
from src.decorators import answer_callback, require_state, with_user_and_session
|
from src.decorators import answer_callback, require_state, with_user_and_session
|
||||||
@@ -40,6 +42,10 @@ INTRO_TEXT = """👋 Добро пожаловать!
|
|||||||
|
|
||||||
Нажмите кнопку ниже, чтобы начать."""
|
Нажмите кнопку ниже, чтобы начать."""
|
||||||
|
|
||||||
|
SPECIFY_GENDER_TEXT = """👤 Укажите ваш пол.
|
||||||
|
|
||||||
|
Это необходимо для подбора подходящих сценариев для озвучивания."""
|
||||||
|
|
||||||
NO_MORE_SCENARIOS_TEXT = """📭 Пока нет доступных сценариев для озвучивания.
|
NO_MORE_SCENARIOS_TEXT = """📭 Пока нет доступных сценариев для озвучивания.
|
||||||
|
|
||||||
Вы получите уведомление, когда появятся новые сценарии."""
|
Вы получите уведомление, когда появятся новые сценарии."""
|
||||||
@@ -82,6 +88,15 @@ def get_intro_keyboard() -> InlineKeyboardMarkup:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_specify_gender_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[InlineKeyboardButton("👨 Мужской", callback_data="select_gender:male")],
|
||||||
|
[InlineKeyboardButton("👩 Женский", callback_data="select_gender:female")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_show_replica_keyboard() -> InlineKeyboardMarkup:
|
def get_show_replica_keyboard() -> InlineKeyboardMarkup:
|
||||||
return InlineKeyboardMarkup(
|
return InlineKeyboardMarkup(
|
||||||
[
|
[
|
||||||
@@ -343,6 +358,27 @@ async def handle_accept_intro(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Обработчик кнопки 'Принять и продолжить'."""
|
"""Обработчик кнопки 'Принять и продолжить'."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
|
session.state = UserState.SPECIFY_GENDER
|
||||||
|
await query.edit_message_text(
|
||||||
|
SPECIFY_GENDER_TEXT, reply_markup=get_specify_gender_keyboard()
|
||||||
|
)
|
||||||
|
session.last_bot_message_id = query.message.message_id
|
||||||
|
upsert_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
@answer_callback
|
||||||
|
@with_user_and_session
|
||||||
|
@require_state(UserState.SPECIFY_GENDER)
|
||||||
|
async def handle_select_gender(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE, user: User, session: UserSession
|
||||||
|
) -> None:
|
||||||
|
"""Обработчик выбора пола."""
|
||||||
|
query = update.callback_query
|
||||||
|
gender = query.data.replace("select_gender:", "")
|
||||||
|
|
||||||
|
update_user_gender(user.id, gender)
|
||||||
|
user.gender = gender
|
||||||
|
|
||||||
track = find_available_track(user.id)
|
track = find_available_track(user.id)
|
||||||
|
|
||||||
if not track:
|
if not track:
|
||||||
@@ -355,7 +391,10 @@ async def handle_accept_intro(
|
|||||||
session.speaker_id = speaker_id
|
session.speaker_id = speaker_id
|
||||||
session.replica_index = 0
|
session.replica_index = 0
|
||||||
await query.edit_message_text(format_first_replica(session))
|
await query.edit_message_text(format_first_replica(session))
|
||||||
logger.info(f"User {user.id} started track {scenario_id}/{speaker_id}")
|
logger.info(
|
||||||
|
f"User {user.id} selected gender {gender}, "
|
||||||
|
f"started track {scenario_id}/{speaker_id}"
|
||||||
|
)
|
||||||
|
|
||||||
session.last_bot_message_id = query.message.message_id
|
session.last_bot_message_id = query.message.message_id
|
||||||
upsert_user_session(session)
|
upsert_user_session(session)
|
||||||
@@ -559,6 +598,10 @@ async def handle_exit_admin(
|
|||||||
# Показываем соответствующее сообщение
|
# Показываем соответствующее сообщение
|
||||||
if session.state == UserState.INTRO:
|
if session.state == UserState.INTRO:
|
||||||
await query.edit_message_text(INTRO_TEXT, reply_markup=get_intro_keyboard())
|
await query.edit_message_text(INTRO_TEXT, reply_markup=get_intro_keyboard())
|
||||||
|
elif session.state == UserState.SPECIFY_GENDER:
|
||||||
|
await query.edit_message_text(
|
||||||
|
SPECIFY_GENDER_TEXT, reply_markup=get_specify_gender_keyboard()
|
||||||
|
)
|
||||||
elif session.state == UserState.NO_MORE_SCENARIOS:
|
elif session.state == UserState.NO_MORE_SCENARIOS:
|
||||||
await query.edit_message_text(NO_MORE_SCENARIOS_TEXT)
|
await query.edit_message_text(NO_MORE_SCENARIOS_TEXT)
|
||||||
elif session.state == UserState.FIRST_REPLICA:
|
elif session.state == UserState.FIRST_REPLICA:
|
||||||
@@ -605,14 +648,21 @@ async def handle_confirm_upload(
|
|||||||
|
|
||||||
del context.user_data["pending_scenario"]
|
del context.user_data["pending_scenario"]
|
||||||
|
|
||||||
|
# Получаем полы, представленные в новом сценарии
|
||||||
|
scenario_genders = get_scenario_genders(pending["id"])
|
||||||
|
|
||||||
# Уведомляем и переводим пользователей в NO_MORE_SCENARIOS на новый сценарий
|
# Уведомляем и переводим пользователей в NO_MORE_SCENARIOS на новый сценарий
|
||||||
for waiting_user_id in get_users_in_state(UserState.NO_MORE_SCENARIOS):
|
for waiting_user_id in get_users_in_state(UserState.NO_MORE_SCENARIOS):
|
||||||
try:
|
try:
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT telegram_id FROM users WHERE id = ?", (waiting_user_id,)
|
"SELECT telegram_id, gender FROM users WHERE id = ?",
|
||||||
|
(waiting_user_id,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if row:
|
if row:
|
||||||
|
user_gender = row[1]
|
||||||
|
# Уведомляем только если пол пользователя представлен в сценарии
|
||||||
|
if user_gender and user_gender in scenario_genders:
|
||||||
waiting_session = get_user_session(waiting_user_id)
|
waiting_session = get_user_session(waiting_user_id)
|
||||||
if waiting_session:
|
if waiting_session:
|
||||||
track = find_available_track(waiting_user_id)
|
track = find_available_track(waiting_user_id)
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ def load_scenario_from_json(scenario_id: str, json_data: list[dict]) -> int:
|
|||||||
if get_scenario(scenario_id):
|
if get_scenario(scenario_id):
|
||||||
raise ValueError(f"Сценарий {scenario_id} уже существует")
|
raise ValueError(f"Сценарий {scenario_id} уже существует")
|
||||||
|
|
||||||
replicas_data: list[tuple[int, int, str]] = []
|
replicas_data: list[tuple[int, int, str, str]] = []
|
||||||
for idx, item in enumerate(json_data):
|
for idx, item in enumerate(json_data):
|
||||||
if "text" not in item or "speaker_id" not in item:
|
if "text" not in item or "speaker_id" not in item or "gender" not in item:
|
||||||
raise ValueError(f"Некорректный формат реплики #{idx}")
|
raise ValueError(f"Некорректный формат реплики #{idx}")
|
||||||
replicas_data.append((item["speaker_id"], idx, item["text"]))
|
replicas_data.append((item["speaker_id"], idx, item["text"], item["gender"]))
|
||||||
|
|
||||||
create_scenario(scenario_id)
|
create_scenario(scenario_id)
|
||||||
create_replicas(scenario_id, replicas_data)
|
create_replicas(scenario_id, replicas_data)
|
||||||
@@ -53,6 +53,13 @@ def parse_scenario_file(file_content: bytes) -> tuple[list[dict], str | None]:
|
|||||||
return [], f"Реплика #{idx}: отсутствует поле 'speaker_id'"
|
return [], f"Реплика #{idx}: отсутствует поле 'speaker_id'"
|
||||||
if not isinstance(item["speaker_id"], int):
|
if not isinstance(item["speaker_id"], int):
|
||||||
return [], f"Реплика #{idx}: 'speaker_id' должен быть числом"
|
return [], f"Реплика #{idx}: 'speaker_id' должен быть числом"
|
||||||
|
if "gender" not in item:
|
||||||
|
return [], f"Реплика #{idx}: отсутствует поле 'gender'"
|
||||||
|
if item["gender"] not in ["male", "female"]:
|
||||||
|
return (
|
||||||
|
[],
|
||||||
|
f"Реплика #{idx}: 'gender' должен быть 'male' или 'female'",
|
||||||
|
)
|
||||||
|
|
||||||
return data, None
|
return data, None
|
||||||
|
|
||||||
@@ -69,16 +76,25 @@ def get_scenario_info(json_data: list[dict]) -> dict:
|
|||||||
|
|
||||||
def find_available_track(user_id: int) -> tuple[str, int] | None:
|
def find_available_track(user_id: int) -> tuple[str, int] | None:
|
||||||
"""
|
"""
|
||||||
Находит доступную дорожку для пользователя.
|
Находит доступную дорожку для пользователя с учётом пола.
|
||||||
Приоритет:
|
Приоритет:
|
||||||
1. Дорожки, которые никто не начал озвучивать
|
1. Дорожки, которые никто не начал озвучивать
|
||||||
2. Дорожки, которые кто-то начал, но не закончил
|
2. Дорожки, которые кто-то начал, но не закончил
|
||||||
3. Дорожки с готовой озвучкой (для дополнительных записей)
|
3. Дорожки с готовой озвучкой (для дополнительных записей)
|
||||||
|
|
||||||
Пользователь не может озвучивать две разные дорожки в одном сценарии.
|
Пользователь не может озвучивать две разные дорожки в одном сценарии.
|
||||||
|
Учитывается пол пользователя и пол дорожек.
|
||||||
Возвращает (scenario_id, speaker_id) или None.
|
Возвращает (scenario_id, speaker_id) или None.
|
||||||
"""
|
"""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
|
# Получаем пол пользователя
|
||||||
|
user_gender_row = conn.execute(
|
||||||
|
"SELECT gender FROM users WHERE id = ?", (user_id,)
|
||||||
|
).fetchone()
|
||||||
|
if not user_gender_row or not user_gender_row[0]:
|
||||||
|
return None
|
||||||
|
user_gender = user_gender_row[0]
|
||||||
|
|
||||||
# Сценарии, в которых пользователь уже записывает дорожку
|
# Сценарии, в которых пользователь уже записывает дорожку
|
||||||
user_scenarios = conn.execute(
|
user_scenarios = conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -88,13 +104,16 @@ def find_available_track(user_id: int) -> tuple[str, int] | None:
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
user_scenario_ids = {row[0] for row in user_scenarios}
|
user_scenario_ids = {row[0] for row in user_scenarios}
|
||||||
|
|
||||||
# Все дорожки (scenario_id, speaker_id) с количеством реплик
|
# Все дорожки (scenario_id, speaker_id) с количеством реплик и полом
|
||||||
|
# Учитываем только дорожки, где пол совпадает с полом пользователя
|
||||||
all_tracks = conn.execute(
|
all_tracks = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT scenario_id, speaker_id, COUNT(*) as replica_count
|
SELECT scenario_id, speaker_id, COUNT(*) as replica_count, gender
|
||||||
FROM replicas
|
FROM replicas
|
||||||
|
WHERE gender = ?
|
||||||
GROUP BY scenario_id, speaker_id
|
GROUP BY scenario_id, speaker_id
|
||||||
"""
|
""",
|
||||||
|
(user_gender,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
# Статистика записей по дорожкам
|
# Статистика записей по дорожкам
|
||||||
|
|||||||
Reference in New Issue
Block a user