Анкетирование и логика подбора тренировок
Детальный анализ всех опросов и алгоритма рекомендации тренировок в проекте.
1. Общая схема
В боте используются три типа анкетирования:
| Тип | Когда | Куда сохраняется | Для чего используется |
|---|---|---|---|
| Предварительная анкета | При первом входе (/start → «Заполнить анкету») | user_surveys + обновление users | Профиль пользователя: пол, возраст, цели, ограничения, уровень. Пробный период 7 дней. |
| Опрос перед тренировкой | «Мои тренировки» → опросник | pre_workout_checkins | Подбор конкретного видео: блок + видео внутри блока (правила + ChatGPT). |
| Опрос после тренировки | «Завершить тренировку» | check_ins | Оценка нагрузки и самочувствия; опционально — соответствие пожеланиям. |
┌─────────────────────────────────────────────────────────────────────────────┐
│ ЖИЗНЕННЫЙ ЦИКЛ ПОЛЬЗОВАТЕЛЯ │
└─────────────────────────────────────────────────────────────────────────────┘
/start (новый) «Мои тренировки» «Завершить тренировку»
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Предваритель-│ │ Pre-workout │ │ Post-workout│
│ ная анкета │ │ опрос │ │ опрос │
│ (имя, пол, │ │ (самочувст- │ Тренировка │ (нагрузка, │
│ возраст, │ │ вие, энергия│ ◄──────────►│ до/после) │
│ цели, │ │ тело, │ │ │
│ здоровье) │ │ пожелания, │ │ │
└──────┬───────┘ │ длительность│ └──────┬──────┘
│ └──────┬──────┘ │
▼ ▼ ▼
user_surveys + pre_workout_ check_ins
users (level, checkins │
restrictions, (load_rating,
goals) wellbeing_before,
│ │ wellbeing_after)
└────────────────────────┼─────────────────────────┘
│
▼
RecommendationService + AIService
→ блок → видео (с учётом противопоказаний) 2. Предварительная анкета (онбординг)
Точка входа: /start → кнопка «Заполнить анкету» (start_questionnaire).
Файлы: handlers/start.py, состояния PreliminarySurveyStates, services/survey_service.py — save_preliminary_survey(), update_user_from_survey().
2.1. Последовательность шагов
| Шаг | Состояние FSM | Вопрос / действие | Сохранение в state / БД |
|---|---|---|---|
| 1 | block1_name | Как к тебе обращаться? (имя из TG или своё) | preferred_name; при первом ответе создаётся запись в users. |
| 2 | block1_gender | Пол (Мужской / Женский / Другое) | gender |
| 3 | block1_age | Возраст (число 10–120) | age |
| 4 | block1_country | Страна проживания | country |
| 5 | — | Подтверждение профиля, переход к блоку 2 | — |
| 6 | block2_difficulties | Сложности с внедрением спорта (множественный выбор) | difficulties |
| 7 | block3_experience | Опыт в спорте | experience_level |
| 8 | block4_health | Ограничения по здоровью (в т.ч. «Ничего из вышеперечисленного») | health_limitations |
| 9 | block5_equipment | Оборудование / инвентарь | equipment |
| 10 | block7_completion | Завершение: сохранение анкеты, обновление пользователя, активация триала | UserSurvey + обновление User |
После завершения вызываются:
survey_service.save_preliminary_survey(...)— запись вuser_surveys;survey_service.update_user_from_survey(user, survey)— перенос вusers: gender, age, country, equipment, restrictions (из health_limitations), goals (из difficulties + experience_level);- активация пробного периода (7 дней).
2.2. Связь с подбором тренировок
- Уровень (user.level): по умолчанию 1; может меняться в настройках или по результатам пост-опроса (корректировка уровня).
- Ограничения (user.restrictions): используются в
RecommendationService.check_contraindications()— при несовместимости с блоком выполняется fallback на блок 2 (мобильность). - Цели (user.goals): передаются в ChatGPT при выборе конкретного видео из блока (
ai_service.recommend_video_from_block()).
3. Опрос перед тренировкой (pre-workout)
Точка входа: «Мои тренировки» (my_trainings) или «Мои тренировки» из меню → start_pre_workout / _send_pre_greeting. После приветствия пользователь нажимает «Продолжить» → pre_block1_continue.
Файлы: handlers/main_menu.py (состояния PreWorkoutStates), services/survey_service.py — save_pre_workout_checkin().
Есть два сценария опроса перед тренировкой (оба ведут к подбору видео):
- Основной (используется сейчас): блоки 1 → 2 → 3 → 4 → 4.5 → 5 → 5.5 → 6.
- Сокращённый (legacy): «Готов к тренировке» → 4 коротких вопроса (настроение, энергия, сон, ограничения) → подбор.
3.1. Основной опрос (блоки 1–6)
| Блок | Состояние FSM | Вопрос | Варианты / поле | Коэффициент | Куда пишется в state |
|---|---|---|---|---|---|
| 1 | block1_greeting | Приветствие, готовность к короткому чеку | Продолжить / Отмена | — | При «Продолжить» — переход к блоку 2. В Redis: pre_workout_started (TTL 60 мин). |
| 2 | block2_wellbeing | Как себя чувствуешь? | Полон(а) энергии, Нормально, Устал(а), Плохо, Свой вариант | wellbeing_coefficient: 1.5 / 1.0 / 0.7 / 0.5 | pre_wellbeing, wellbeing_coefficient |
| 3 | block3_energy_mood | Уровень энергии и настроение? | На пике, Средний, Нужна зарядка, Настроение не очень | energy_coefficient: 1.5 / 1.0 / 0.7 / 0.5 | pre_energy_level, pre_mood, energy_coefficient |
| 4 | block4_body | Что стоит учесть перед тренировкой? | Всё в порядке, Боль в мышцах, Дискомфорт, Что-то болит, Другое | body_coefficient: 1.5 / 1.0 / 0.7 / 0.5 | pre_body_sensations, body_coefficient |
| 4.5 | block4_5_wishes | Пожелания по тренировке? | Нет, Спина/шея, Растяжка, Пресс, Ягодицы, После сидения, Свой вариант | — | pre_wishes |
| 5 | block5_duration | Продолжительность и тип | 15–20 мин, ~30 мин, 30+ мин, Растяжка/разминка | — | pre_workout_duration, pre_workout_type |
| 5.5 | block5_5_wellbeing_rating | Самочувствие сейчас (1–5) | 1–5 | — | pre_wellbeing_rating |
| 6 | block6_completion | Всё готово, начинаем? | Да, поехали! / Подожди | — | При «Да» — сохранение чек-ина и подбор тренировки. |
При завершении вызывается survey_service.save_pre_workout_checkin(...) с полями: wellbeing, energy_level, mood, body_sensations, workout_duration, workout_type, wishes, wellbeing_rating.
3.2. Коэффициент интенсивности (intensity_coefficient)
Считается один раз перед вызовом рекомендации:
intensity_coefficient = (wellbeing_coef + energy_coef + body_coef) / 3
- wellbeing_coef — из блока 2 (1.5 / 1.0 / 0.7 / 0.5).
- energy_coef — из блока 3 (1.5 / 1.0 / 0.7 / 0.5).
- body_coef — из блока 4 (1.5 / 1.0 / 0.7 / 0.5).
Используется в RecommendationService.suggest_block_by_state():
- < 0.8 — восстановительные блоки (1, 2);
- 0.8–1.2 — стандартные (3, 4, 6);
- ≥ 1.2 — интенсивные (5, 7, 8) с учётом уровня.
3.3. Преобразование в формат RecommendationService
| Данные из опроса | Поле для рекомендации | Правило |
|---|---|---|
pre_wellbeing | mood (mapped_mood) | Словарь: «Полна/полон энергии» → отлично, «Нормально…» → нормально, «Немного устал(а)» → устал, «Утомлён(а)…» → плохо. |
pre_energy_level | energy (mapped_energy) | Высокая → Высокий, Средняя → Средний, Низкая → Низкий. |
| — | sleep | В основном сценарии не спрашивается: всегда «Нормально». |
pre_body_sensations | limits | По ключевым словам: «болит»/«боль» → «Плохо себя чувствую», «дискомфорт»/«неприятные» → «Лёгкое недомогание», иначе «Нет». |
pre_wishes | user_goals | Добавляются к существующим целям пользователя (user.goals) при вызове AI. |
3.4. Legacy-опрос (4 шага)
Если пользователь попадает в сценарий с кнопкой «Готов(а) к тренировке» (pre_ready_yes), запускается короткий опрос:
- pre_q1: настроение →
pre_mood. - pre_q2: уровень энергии →
pre_energy. - pre_q3: сон →
pre_sleep. - pre_q4: ограничения →
pre_limits.
Далее вызывается тот же get_recommended_video(mood, energy, sleep, limits, ...).
4. Опрос после тренировки (post-workout)
Точка входа: нажатие «Завершить тренировку» (finish_training_<video_id>).
Файлы: handlers/main_menu.py (состояния CheckInStates), services/user_service.py — create_check_in().
4.1. Последовательность
| Шаг | Состояние FSM | Вопрос | Шкала | Поле в state / БД |
|---|---|---|---|---|
| 1 | waiting_for_load_rating | Подошла ли нагрузка? | 1 (совсем тяжело) – 5 (идеально) | checkin_load_rating → CheckIn.load_rating |
| 2 | waiting_for_wellbeing_before | Самочувствие до тренировки? | 1 (никак) – 5 (полна/полон ресурса) | checkin_wellbeing_before → CheckIn.wellbeing_before |
| 3 | waiting_for_wellbeing_after | Самочувствие после? | 1 (разбит/разбита) – 5 (в теле и в моменте) | checkin_wellbeing_after → CheckIn.wellbeing_after |
| 4 (опц.) | waiting_for_wishes_feedback | Если перед тренировкой был «свой вариант» пожеланий — насколько тренировка соответствовала? | 1–5 | Сохраняется в логи/детали. |
После шага 3 (или 4) создаётся запись в check_ins: load_rating, wellbeing_before, wellbeing_after, mood="completed" и заметка о видео. Показывается случайное поддерживающее сообщение и кнопки «Хочу ещё одну тренировку» и «Главное меню».
Самочувствие до тренировки для пост-опроса: берётся из последнего pre-workout чек-ина (survey_service.get_user_last_checkin(user.id).wellbeing_rating) и подставляется в вопрос 2; при сохранении в CheckIn пишется переданный пользователем ответ (wellbeing_before).
5. Логика подбора тренировок
Цепочка: правила по состоянию → выбор блока → проверка противопоказаний → фильтр по уровню → выбор видео внутри блока (ChatGPT или случайный fallback).
5.1. Выбор блока (suggest_block_by_state)
Вход: mood, energy, sleep, limits, user_level (1–7), intensity_coefficient.
Порядок правил:
- Ограничения или плохой сон: limits in («Лёгкое недомогание», «Плохо себя чувствую») или sleep == «Очень плохо» → блок 2 (мобильность).
- Низкий коэффициент интенсивности (< 0.8): Восстановление: уровень ≤ 2 → блок 1, иначе блок 2.
- Высокий коэффициент (≥ 1.2): user_level ≥ 5 → блок 5 (Табата); user_level ≥ 3 → блок 7 (функциональные); иначе блок 3 (пилатес).
- Средний коэффициент (0.8–1.2): Низкая энергия или «устав» в mood → блок 1 (если level ≤ 2) иначе блок 2. Высокая энергия и отличное настроение → блок 5 (если level ≥ 4) иначе блок 3. Средняя энергия → блок 3. По умолчанию → блок 2.
Смысл блоков:
- Блок 1: короткие узконаправленные (МФР, нейро-фитнес), восстановление.
- Блок 2: мобильность, растяжка, дыхание.
- Блок 3: пилатес. Блок 4: пресс. Блок 5: Табата (интервальные). Блок 6: с резиной. Блок 7: функциональные. Блок 8: интенсивные.
5.2. Противопоказания и fallback блока
get_recommended_block_with_fallback() сначала получает блок из suggest_block_by_state(), затем вызывает check_contraindications(block_number, user_restrictions_text, db). Проверяются ключевые слова: грыжи, операции, беременность, диастаз. Если блок противопоказан → возвращается блок 2 как безопасный.
5.3. Выбор конкретного видео (get_recommended_video)
- Рекомендованный блок (с учётом противопоказаний):
get_recommended_block_with_fallback(...). - Список видео блока:
video_service.get_videos_by_block(recommended_block). - Если видео нет — пробуют блок 2; если и там пусто — возвращают
None. - Фильтр по уровню: оставляют видео с
abs(v.level - user_level) <= 1. Если таких нет — берут все видео блока. - Выбор одного видео: если передан
ai_service— вызываетсяai_service.recommend_video_from_block(...); ответ GPT — JSON { "video_id", "video_title" }; при ошибке — случайный выбор из filtered_videos.
5.4. Роль ChatGPT
В промпт передаются: описание блока, данные анкеты (настроение, энергия, сон, ограничения), уровень пользователя, цели, противопоказания, список доступных видео. Задача модели: выбрать одну тренировку с учётом состояния, уровня, целей и безопасности. Ответ парсится; проверяется, что video_id входит в переданный список.
5.5. Fallback при отсутствии рекомендации
Если get_recommended_video() вернул None, вызывается video_service.get_next_video_no_repeat(user_id, user_level) — случайное видео по уровню (или уровень 1).
5.6. Программа тренировок (selected_program)
Если у пользователя задана программа (user.selected_program), в legacy-ветке pre-workout сначала вызывается video_service.get_next_program_video(...). Если видео по программе найдено — оно и отдаётся; иначе используется интеллектуальная рекомендация через get_recommended_video(). В основном сценарии (блоки 1–6) программа не используется — всегда подбор по состоянию.
6. Схемы потоков данных
6.1. Pre-workout → рекомендация (основной сценарий)
[Блок 2] Самочувствие → pre_wellbeing, wellbeing_coefficient (1.5/1.0/0.7/0.5)
[Блок 3] Энергия/настроение → pre_energy_level, pre_mood, energy_coefficient
[Блок 4] Тело/ощущения → pre_body_sensations, body_coefficient
[Блок 4.5] Пожелания → pre_wishes
[Блок 5] Длительность → pre_workout_duration, pre_workout_type
[Блок 5.5] Оценка 1–5 → pre_wellbeing_rating
│
▼
intensity_coefficient = (wellbeing + energy + body) / 3
│
▼
mood = map(pre_wellbeing) energy = map(pre_energy_level)
sleep = "Нормально" limits = from pre_body_sensations
user_goals += pre_wishes
│
▼
RecommendationService.get_recommended_block_with_fallback(...)
│
▼
VideoService.get_videos_by_block(block) → фильтр по level ± 1
│
▼
AIService.recommend_video_from_block(...) или random
│
▼
Одно видео → WebApp / кнопка «Завершить тренировку» 6.2. Сохранение и использование чек-инов
Pre-workout завершён Post-workout завершён
│ │
▼ ▼
save_pre_workout_checkin() create_check_in(
→ pre_workout_checkins load_rating,
(wellbeing, energy_level, wellbeing_before,
mood, body_sensations, wellbeing_after)
workout_duration, → check_ins
workout_type, wishes,
wellbeing_rating) wellbeing_before в пост-опросе может подставляться из последнего PreWorkoutCheckin.wellbeing_rating (для отображения вопроса), но в CheckIn сохраняется ответ пользователя после тренировки.
7. Таблицы БД
user_surveys (предварительная анкета)
| Поле | Тип | Описание |
|---|---|---|
| user_id | FK users | Пользователь |
| gender, age, country | string | Блок 1 |
| difficulties | JSON (array) | Сложности |
| experience_level | string | Опыт |
| health_limitations | JSON (array) | Здоровье |
| equipment | JSON (array) | Оборудование |
| planning_preference, workout_duration | string | Опционально |
pre_workout_checkins (опрос перед тренировкой)
| Поле | Тип | Описание |
|---|---|---|
| user_id | FK users | Пользователь |
| wellbeing | string | Самочувствие (блок 2) |
| energy_level | string | Энергия (блок 3) |
| mood | string | Настроение (блок 3) |
| body_sensations | string | Тело/ощущения (блок 4) |
| workout_duration | string | Длительность (блок 5) |
| workout_type | string | Тип (workout/stretch) |
| wishes | string | Пожелания (блок 4.5) |
| wellbeing_rating | int | Оценка 1–5 (блок 5.5) |
| created_at | datetime | Время чека |
check_ins (опрос после тренировки)
| Поле | Тип | Описание |
|---|---|---|
| user_id | FK users | Пользователь |
| date | datetime | Время чека |
| mood | string | Часто "completed" |
| notes | text | Заметка (например, id видео) |
| level_adjustment | int | Корректировка уровня |
| load_rating | int | Нагрузка 1–5 |
| wellbeing_before | int | Самочувствие до 1–5 |
| wellbeing_after | int | Самочувствие после 1–5 |
users (поля, влияющие на рекомендации)
- level (1–7) — фильтр видео и выбор блока.
- goals (JSON) — передаются в ChatGPT при выборе видео.
- restrictions (JSON) — проверка противопоказаний блоков; при совпадении — переход на блок 2.
- selected_program — при использовании программы: следующее видео по программе (в legacy pre-workout).
Иллюстрация: от ответов к блоку
Ответы пользователя Коэффициент Блок
─────────────────────────────────────────────────────────────────────────
«Полна энергии» + «На пике» + (1.5+1.5+1.5)/3 = 1.5 5 или 7
«Всё в порядке» ≥1.2, level учтён (Табата/функционал)
«Нормально» + «Средний» + (1.0+1.0+1.0)/3 = 1.0 3
«Всё в порядке» 0.8–1.2, средняя энергия (Пилатес)
«Устала» + «Нужна зарядка» + (0.7+0.7+1.0)/3 ≈ 0.8 2 или 1
«Лёгкая боль в мышцах» граница 0.8 (Мобильность)
«Плохо себя чувствую» + любое limits = «Плохо…» 2
→ всегда блок 2 (Безопасный) 