feat: мини-игры, premium подписка, улучшенные контексты

Мини-игры (/games):
- Speed Round: 10 раундов, 10 секунд на ответ, очки за скорость
- Match Pairs: 5 слов + 5 переводов, соединить пары

Premium-функции:
- Поля is_premium и premium_until для пользователей
- AI режим проверки ответов (учитывает синонимы)
- Batch проверка всех ответов одним запросом

Улучшения:
- Примеры использования для всех добавляемых слов
- Разбиение переводов по запятой на отдельные записи
- Полные предложения в контекстах (без ___)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-10 19:42:10 +03:00
parent b74ea2170c
commit adc8a6bf8e
18 changed files with 1819 additions and 34 deletions

View File

@@ -398,6 +398,8 @@ class AIService:
"word": "исходное слово",
"translation": "перевод",
"transcription": "транскрипция (IPA или ромадзи для японского)",{furigana_instruction}
"example": "короткий пример использования на {source_lang}",
"example_translation": "перевод примера на {translation_lang}"
}},
...
]
@@ -405,7 +407,7 @@ class AIService:
Важно:
- Верни только JSON массив, без дополнительного текста
- Сохрани порядок слов как в исходном списке
- Для каждого слова укажи точный перевод и транскрипцию"""
- Для каждого слова укажи точный перевод, транскрипцию и короткий пример"""
try:
logger.info(f"[AI Request] translate_words_batch: {len(words)} words, {source_lang} -> {translation_lang}")
@@ -498,6 +500,156 @@ class AIService:
"score": 0
}
async def check_translation(
self,
word: str,
correct_translation: str,
user_answer: str,
source_lang: str = "en",
target_lang: str = "ru",
user_id: Optional[int] = None
) -> Dict:
"""
Проверить перевод слова с помощью ИИ (для мини-игр)
Args:
word: Оригинальное слово
correct_translation: Эталонный перевод
user_answer: Ответ пользователя
source_lang: Язык оригинального слова
target_lang: Язык перевода
user_id: ID пользователя в БД
Returns:
Dict с результатом проверки
"""
prompt = f"""Проверь перевод слова.
Слово ({source_lang}): {word}
Эталонный перевод ({target_lang}): {correct_translation}
Ответ пользователя: {user_answer}
Определи, правильный ли перевод пользователя. Учитывай:
- Синонимы и близкие по смыслу слова
- Разные формы слова (единственное/множественное число)
- Небольшие опечатки
Верни JSON:
{{
"is_correct": true/false,
"feedback": "краткое пояснение (почему верно/неверно, какой вариант лучше)"
}}"""
try:
logger.info(f"[AI Request] check_translation: word='{word}', user='{user_answer}'")
messages = [
{"role": "system", "content": "Ты - лингвист, проверяющий переводы. Будь справедлив и учитывай синонимы."},
{"role": "user", "content": prompt}
]
response_data = await self._make_request(messages, temperature=0.2, user_id=user_id)
result = json.loads(response_data['choices'][0]['message']['content'])
logger.info(f"[AI Response] check_translation: is_correct={result.get('is_correct', False)}")
return result
except Exception as e:
logger.error(f"[AI Error] check_translation: {type(e).__name__}: {str(e)}")
# В случае ошибки делаем простое сравнение
is_correct = user_answer.lower().strip() == correct_translation.lower().strip()
return {
"is_correct": is_correct,
"feedback": ""
}
async def check_translations_batch(
self,
answers: List[Dict],
source_lang: str = "en",
target_lang: str = "ru",
user_id: Optional[int] = None
) -> List[Dict]:
"""
Проверить несколько переводов одним запросом (для мини-игр)
Args:
answers: Список словарей с ключами: word, correct_translation, user_answer
source_lang: Язык оригинальных слов
target_lang: Язык перевода
user_id: ID пользователя в БД
Returns:
Список словарей с результатами проверки
"""
if not answers:
return []
# Формируем список для проверки
answers_text = ""
for i, ans in enumerate(answers, 1):
answers_text += f"{i}. {ans['word']} → эталон: {ans['correct_translation']} | ответ: {ans['user_answer']}\n"
prompt = f"""Проверь переводы слов с {source_lang} на {target_lang}.
{answers_text}
Для каждого слова определи, правильный ли перевод. Учитывай:
- Синонимы и близкие по смыслу слова
- Разные формы слова
- Небольшие опечатки
Верни JSON массив:
[
{{"index": 1, "is_correct": true/false, "feedback": "краткое пояснение", "user_answer_meaning": "что означает ответ пользователя на {source_lang}, если это валидное слово"}},
...
]
user_answer_meaning - переведи ответ пользователя обратно на {source_lang}, чтобы показать что он на самом деле написал. Если ответ бессмысленный - оставь пустым."""
try:
logger.info(f"[AI Request] check_translations_batch: {len(answers)} answers")
messages = [
{"role": "system", "content": "Ты - лингвист, проверяющий переводы. Будь справедлив и учитывай синонимы. Отвечай только JSON массивом."},
{"role": "user", "content": prompt}
]
response_data = await self._make_request(messages, temperature=0.2, user_id=user_id)
result = json.loads(response_data['choices'][0]['message']['content'])
logger.info(f"[AI Response] check_translations_batch: {len(result)} results")
# Преобразуем в удобный формат
results_map = {r['index']: r for r in result}
final_results = []
for i, ans in enumerate(answers, 1):
if i in results_map:
final_results.append({
'is_correct': results_map[i].get('is_correct', False),
'feedback': results_map[i].get('feedback', ''),
'user_answer_meaning': results_map[i].get('user_answer_meaning', '')
})
else:
# Fallback на простое сравнение
is_correct = ans['user_answer'].lower().strip() == ans['correct_translation'].lower().strip()
final_results.append({'is_correct': is_correct, 'feedback': '', 'user_answer_meaning': ''})
return final_results
except Exception as e:
logger.error(f"[AI Error] check_translations_batch: {type(e).__name__}: {str(e)}")
# В случае ошибки делаем простое сравнение для всех
return [
{
'is_correct': ans['user_answer'].lower().strip() == ans['correct_translation'].lower().strip(),
'feedback': '',
'user_answer_meaning': ''
}
for ans in answers
]
async def generate_fill_in_sentence(self, word: str, learning_lang: str = "en", translation_lang: str = "ru", user_id: Optional[int] = None) -> Dict:
"""
Сгенерировать предложение с пропуском для заданного слова
@@ -649,15 +801,17 @@ class AIService:
"results": [
{{
"sentence": "предложение (с ___ для fill_blank)",
"full_sentence": "полное предложение БЕЗ пропуска",
"answer": "слово для пропуска (только для fill_blank)",
"translation": "перевод на {translation_lang}"
"translation": "ПОЛНЫЙ перевод предложения на {translation_lang} (БЕЗ пропусков, БЕЗ слов на {learning_lang})"
}}
]
}}
Важно:
- Для fill_blank: замени целевое слово на ___, укажи answer
- Для fill_blank: замени целевое слово на ___, укажи answer и full_sentence
- Для sentence_translate: просто предложение со словом, answer не нужен
- translation должен быть ПОЛНЫМ переводом на {translation_lang}, без ___ и без слов на {learning_lang}
- Предложения должны быть простыми (5-10 слов)
- Контекст должен подсказывать правильное слово{furigana_instruction}
- Верни результаты В ТОМ ЖЕ ПОРЯДКЕ что и задания"""
@@ -842,7 +996,8 @@ class AIService:
"word": "слово на {learning_lang} (в базовой форме)",
"translation": "перевод на {translation_lang}",
"transcription": "транскрипция в IPA (для английского) или хирагана (для японского)",
"context": "предложение из текста на {learning_lang}, где используется это слово"
"example": "предложение из текста на {learning_lang}, где используется это слово",
"example_translation": "перевод этого предложения на {translation_lang}"
}}
]
}}