feat: multiple translations with context, improved task examples

- Add WordTranslation model for storing multiple translations per word
- AI generates translations with example sentences and their translations
- Show example usage after answering tasks (learning + interface language)
- Save translations to word_translations table when adding words from tasks
- Improve word exclusion in new_words mode (stronger prompt + client filtering)
- Add migration for word_translations table

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-06 21:29:41 +03:00
parent 63e2615243
commit d937b37a3b
10 changed files with 543 additions and 30 deletions

View File

@@ -111,6 +111,98 @@ class AIService:
"difficulty": "A1"
}
async def translate_word_with_contexts(
self,
word: str,
source_lang: str = "en",
translation_lang: str = "ru",
max_translations: int = 3
) -> Dict:
"""
Перевести слово и получить несколько переводов с контекстами
Args:
word: Слово для перевода
source_lang: Язык исходного слова (ISO2)
translation_lang: Язык перевода (ISO2)
max_translations: Максимальное количество переводов
Returns:
Dict с переводами, каждый с примером предложения
"""
prompt = f"""Переведи слово/фразу "{word}" с языка {source_lang} на {translation_lang}.
Если у слова есть несколько значений в разных контекстах, дай до {max_translations} разных переводов.
Для каждого перевода дай пример предложения, показывающий это значение.
Верни ответ строго в формате JSON:
{{
"word": "исходное слово на {source_lang}",
"transcription": "транскрипция в IPA (если применимо)",
"category": "основная категория слова",
"difficulty": "уровень сложности (A1/A2/B1/B2/C1/C2)",
"translations": [
{{
"translation": "перевод 1 на {translation_lang}",
"context": "пример предложения на {source_lang}, показывающий это значение",
"context_translation": "перевод примера на {translation_lang}",
"is_primary": true
}},
{{
"translation": "перевод 2 на {translation_lang} (если есть другое значение)",
"context": "пример предложения на {source_lang}",
"context_translation": "перевод примера на {translation_lang}",
"is_primary": false
}}
]
}}
Важно:
- Первый перевод должен быть самым распространённым (is_primary: true)
- Давай разные переводы только если слово реально имеет разные значения
- Примеры должны чётко показывать конкретное значение слова
- Верни только JSON, без дополнительного текста"""
try:
logger.info(f"[GPT Request] translate_word_with_contexts: word='{word}', source='{source_lang}', to='{translation_lang}'")
messages = [
{"role": "system", "content": "Ты - помощник для изучения языков. Отвечай только в формате JSON."},
{"role": "user", "content": prompt}
]
response_data = await self._make_openai_request(messages, temperature=0.3)
import json
content = response_data['choices'][0]['message']['content']
# Убираем markdown обёртку если есть
if content.startswith('```'):
content = content.split('\n', 1)[1] if '\n' in content else content[3:]
if content.endswith('```'):
content = content[:-3]
content = content.strip()
result = json.loads(content)
translations_count = len(result.get('translations', []))
logger.info(f"[GPT Response] translate_word_with_contexts: success, {translations_count} translations")
return result
except Exception as e:
logger.error(f"[GPT Error] translate_word_with_contexts: {type(e).__name__}: {str(e)}")
# Fallback в случае ошибки
return {
"word": word,
"transcription": "",
"category": "unknown",
"difficulty": "A1",
"translations": [{
"translation": "Ошибка перевода",
"context": "",
"context_translation": "",
"is_primary": True
}]
}
async def translate_words_batch(
self,
words: List[str],
@@ -294,7 +386,15 @@ class AIService:
"translation": f"Мне нравится {word} каждый день."
}
async def generate_thematic_words(self, theme: str, level: str = "B1", count: int = 10, learning_lang: str = "en", translation_lang: str = "ru") -> List[Dict]:
async def generate_thematic_words(
self,
theme: str,
level: str = "B1",
count: int = 10,
learning_lang: str = "en",
translation_lang: str = "ru",
exclude_words: List[str] = None
) -> List[Dict]:
"""
Сгенерировать подборку слов по теме
@@ -302,12 +402,28 @@ class AIService:
theme: Тема для подборки слов
level: Уровень сложности (A1-C2)
count: Количество слов
learning_lang: Язык изучения
translation_lang: Язык перевода
exclude_words: Список слов для исключения (уже известные)
Returns:
Список словарей с информацией о словах
"""
prompt = f"""Создай подборку из {count} слов на языке {learning_lang} по теме "{theme}" для уровня {level}. Переводы дай на {translation_lang}.
exclude_instruction = ""
exclude_words_set = set()
if exclude_words:
# Ограничиваем список до 100 слов чтобы не раздувать промпт
words_sample = exclude_words[:100]
exclude_words_set = set(w.lower() for w in exclude_words)
exclude_instruction = f"""
⚠️ ЗАПРЕЩЁННЫЕ СЛОВА (НЕ ИСПОЛЬЗОВАТЬ!):
{', '.join(words_sample)}
Эти слова пользователь уже знает. ОБЯЗАТЕЛЬНО выбери ДРУГИЕ слова!"""
prompt = f"""Создай подборку из {count} слов на языке {learning_lang} по теме "{theme}" для уровня {level}. Переводы дай на {translation_lang}.
{exclude_instruction}
Верни ответ в формате JSON:
{{
"theme": "{theme}",
@@ -316,7 +432,8 @@ class AIService:
"word": "слово на {learning_lang}",
"translation": "перевод на {translation_lang}",
"transcription": "транскрипция в IPA (если применимо)",
"example": "пример использования на {learning_lang}"
"example": "пример использования на {learning_lang}",
"example_translation": "перевод примера на {translation_lang}"
}}
]
}}
@@ -339,9 +456,21 @@ class AIService:
import json
result = json.loads(response_data['choices'][0]['message']['content'])
words_count = len(result.get('words', []))
logger.info(f"[GPT Response] generate_thematic_words: success, generated {words_count} words")
return result.get('words', [])
words = result.get('words', [])
# Фильтруем слова которые AI мог вернуть несмотря на инструкцию
if exclude_words_set:
filtered_words = [
w for w in words
if w.get('word', '').lower() not in exclude_words_set
]
filtered_count = len(words) - len(filtered_words)
if filtered_count > 0:
logger.info(f"[GPT Response] generate_thematic_words: filtered out {filtered_count} excluded words")
words = filtered_words
logger.info(f"[GPT Response] generate_thematic_words: success, generated {len(words)} words")
return words
except Exception as e:
logger.error(f"[GPT Error] generate_thematic_words: {type(e).__name__}: {str(e)}")