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:
@@ -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}"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from database.models import Vocabulary, WordSource, LanguageLevel, WordTranslation
|
||||
from typing import List, Optional, Dict
|
||||
import random
|
||||
import re
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database.models import Vocabulary, WordSource, LanguageLevel, WordTranslation
|
||||
|
||||
|
||||
class VocabularyService:
|
||||
@@ -263,6 +266,61 @@ class VocabularyService:
|
||||
|
||||
return new_translation
|
||||
|
||||
@staticmethod
|
||||
async def add_translation_split(
|
||||
session: AsyncSession,
|
||||
vocabulary_id: int,
|
||||
translation: str,
|
||||
context: Optional[str] = None,
|
||||
context_translation: Optional[str] = None,
|
||||
is_primary: bool = True
|
||||
) -> List[WordTranslation]:
|
||||
"""
|
||||
Добавить перевод(ы) к слову, разбивая строку по разделителям.
|
||||
|
||||
Если translation содержит несколько переводов через запятую или точку с запятой,
|
||||
каждый перевод добавляется отдельной записью.
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
vocabulary_id: ID слова в словаре
|
||||
translation: Перевод (может содержать несколько через запятую)
|
||||
context: Пример использования на языке изучения
|
||||
context_translation: Перевод примера
|
||||
is_primary: Является ли первый перевод основным
|
||||
|
||||
Returns:
|
||||
Список созданных переводов
|
||||
"""
|
||||
import re
|
||||
|
||||
# Разбиваем по запятой или точке с запятой
|
||||
parts = re.split(r'[,;]\s*', translation)
|
||||
|
||||
# Очищаем и фильтруем пустые
|
||||
translations = [p.strip() for p in parts if p.strip()]
|
||||
|
||||
if not translations:
|
||||
return []
|
||||
|
||||
created = []
|
||||
for i, tr in enumerate(translations):
|
||||
new_translation = WordTranslation(
|
||||
vocabulary_id=vocabulary_id,
|
||||
translation=tr,
|
||||
context=context if i == 0 else None, # Контекст только для первого перевода
|
||||
context_translation=context_translation if i == 0 else None,
|
||||
is_primary=(is_primary and i == 0) # Только первый - основной
|
||||
)
|
||||
session.add(new_translation)
|
||||
created.append(new_translation)
|
||||
|
||||
await session.commit()
|
||||
for t in created:
|
||||
await session.refresh(t)
|
||||
|
||||
return created
|
||||
|
||||
@staticmethod
|
||||
async def add_translations_bulk(
|
||||
session: AsyncSession,
|
||||
@@ -368,3 +426,103 @@ class VocabularyService:
|
||||
await session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def get_user_word_count(session: AsyncSession, user_id: int) -> int:
|
||||
"""
|
||||
Получить количество слов пользователя
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
|
||||
Returns:
|
||||
Количество слов
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(func.count(Vocabulary.id)).where(Vocabulary.user_id == user_id)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@staticmethod
|
||||
async def get_random_words_for_game(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
count: int = 10,
|
||||
learning_lang: Optional[str] = None
|
||||
) -> List[Vocabulary]:
|
||||
"""
|
||||
Получить случайные слова для мини-игры
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
count: Количество слов
|
||||
learning_lang: Язык изучения для фильтрации
|
||||
|
||||
Returns:
|
||||
Список случайных слов
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(Vocabulary).where(Vocabulary.user_id == user_id)
|
||||
)
|
||||
words = list(result.scalars().all())
|
||||
|
||||
if learning_lang:
|
||||
words = VocabularyService._filter_by_learning_lang(words, learning_lang)
|
||||
|
||||
# Перемешиваем и берём нужное количество
|
||||
random.shuffle(words)
|
||||
return words[:count]
|
||||
|
||||
@staticmethod
|
||||
async def get_random_words_with_translations(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
count: int = 10,
|
||||
learning_lang: Optional[str] = None
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Получить случайные слова для мини-игры вместе со всеми переводами
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
count: Количество слов
|
||||
learning_lang: Язык изучения для фильтрации
|
||||
|
||||
Returns:
|
||||
Список словарей с информацией о словах и их переводах
|
||||
"""
|
||||
# Получаем слова
|
||||
words = await VocabularyService.get_random_words_for_game(
|
||||
session, user_id, count, learning_lang
|
||||
)
|
||||
|
||||
result = []
|
||||
for word in words:
|
||||
# Получаем все переводы для слова
|
||||
translations = await VocabularyService.get_word_translations(session, word.id)
|
||||
|
||||
# Собираем все варианты перевода
|
||||
all_translations = []
|
||||
|
||||
# Основной перевод из vocabulary
|
||||
if word.word_translation:
|
||||
all_translations.append(word.word_translation.lower().strip())
|
||||
|
||||
# Переводы из word_translations
|
||||
for tr in translations:
|
||||
tr_text = tr.translation.lower().strip()
|
||||
if tr_text not in all_translations:
|
||||
all_translations.append(tr_text)
|
||||
|
||||
result.append({
|
||||
'id': word.id,
|
||||
'word': word.word_original,
|
||||
'translation': word.word_translation, # Основной перевод для отображения
|
||||
'all_translations': all_translations, # Все варианты для проверки
|
||||
'transcription': word.transcription
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user