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

@@ -109,6 +109,18 @@ async def start_new_words_tasks(callback: CallbackQuery, state: FSMContext):
# Показываем индикатор загрузки
await callback.message.edit_text(t(lang, 'tasks.generating_new'))
# Получаем слова для исключения:
# 1. Все слова из словаря пользователя
vocab_words = await VocabularyService.get_all_user_word_strings(
session, user.id, learning_lang=user.learning_language
)
# 2. Слова из предыдущих заданий new_words, на которые ответили правильно
correct_task_words = await TaskService.get_correctly_answered_words(
session, user.id
)
# Объединяем списки исключений
exclude_words = list(set(vocab_words + correct_task_words))
# Генерируем новые слова через AI
words = await ai_service.generate_thematic_words(
theme="random everyday vocabulary",
@@ -116,6 +128,7 @@ async def start_new_words_tasks(callback: CallbackQuery, state: FSMContext):
count=5,
learning_lang=user.learning_language,
translation_lang=user.language_interface,
exclude_words=exclude_words if exclude_words else None,
)
if not words:
@@ -132,7 +145,9 @@ async def start_new_words_tasks(callback: CallbackQuery, state: FSMContext):
'question': f"{translate_prompt}: {word.get('word', '')}",
'word': word.get('word', ''),
'correct_answer': word.get('translation', ''),
'transcription': word.get('transcription', '')
'transcription': word.get('transcription', ''),
'example': word.get('example', ''), # Пример на изучаемом языке
'example_translation': word.get('example_translation', '') # Перевод примера
})
await state.update_data(
@@ -226,6 +241,16 @@ async def process_answer(message: Message, state: FSMContext):
if feedback:
result_text += f"💬 {feedback}\n\n"
# Показываем пример использования если есть
example = task.get('example', '')
example_translation = task.get('example_translation', '')
if example:
result_text += f"📖 {t(lang, 'tasks.example_label')}:\n"
result_text += f"<i>{example}</i>\n"
if example_translation:
result_text += f"<i>({example_translation})</i>\n"
result_text += "\n"
# Сохраняем результат в БД
async with async_session_maker() as session:
await TaskService.save_task_result(
@@ -298,6 +323,8 @@ async def add_task_word(callback: CallbackQuery, state: FSMContext):
word = task.get('word', '')
translation = task.get('correct_answer', '')
transcription = task.get('transcription', '')
example = task.get('example', '') # Пример использования как контекст
example_translation = task.get('example_translation', '') # Перевод примера
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
@@ -316,7 +343,7 @@ async def add_task_word(callback: CallbackQuery, state: FSMContext):
return
# Добавляем слово в словарь
await VocabularyService.add_word(
new_word = await VocabularyService.add_word(
session=session,
user_id=user.id,
word_original=word,
@@ -327,6 +354,18 @@ async def add_task_word(callback: CallbackQuery, state: FSMContext):
source=WordSource.AI_TASK
)
# Сохраняем перевод в таблицу word_translations
await VocabularyService.add_translations_bulk(
session=session,
vocabulary_id=new_word.id,
translations=[{
'translation': translation,
'context': example if example else None,
'context_translation': example_translation if example_translation else None,
'is_primary': True
}]
)
await callback.answer(t(lang, 'tasks.word_added', word=word), show_alert=True)

View File

@@ -69,33 +69,47 @@ async def process_word_addition(message: Message, state: FSMContext, word: str):
lang = (user.language_interface if user else 'ru') or 'ru'
processing_msg = await message.answer(t(lang, 'add.searching'))
# Получаем перевод через AI
# Получаем перевод через AI (с несколькими значениями)
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
source_lang = user.learning_language if user else 'en'
ui_lang = user.language_interface if user else 'ru'
word_data = await ai_service.translate_word(word, source_lang=source_lang, translation_lang=ui_lang)
word_data = await ai_service.translate_word_with_contexts(
word, source_lang=source_lang, translation_lang=ui_lang, max_translations=3
)
# Удаляем сообщение о загрузке
await processing_msg.delete()
# Формируем примеры
examples_text = ""
if word_data.get("examples"):
examples_text = "\n\n" + t(lang, 'add.examples_header') + "\n"
for idx, example in enumerate(word_data["examples"][:2], 1):
src = example.get(source_lang) or example.get('en') or example.get('ru') or ''
tr = example.get(ui_lang) or example.get('ru') or example.get('en') or ''
examples_text += f"{idx}. {src}\n <i>{tr}</i>\n"
# Формируем текст с переводами
translations = word_data.get("translations", [])
translations_text = ""
if translations:
# Основной перевод для backward compatibility
primary = next((tr for tr in translations if tr.get('is_primary')), translations[0])
word_data['translation'] = primary.get('translation', '')
translations_text = "\n\n" + t(lang, 'add.translations_header') + "\n"
for idx, tr in enumerate(translations, 1):
marker = "" if tr.get('is_primary') else ""
translations_text += f"{idx}. {marker}<b>{tr.get('translation', '')}</b>\n"
if tr.get('context'):
translations_text += f" <i>«{tr.get('context', '')}»</i>\n"
if tr.get('context_translation'):
translations_text += f" <i>({tr.get('context_translation', '')})</i>\n"
translations_text += "\n"
else:
# Fallback если нет переводов
word_data['translation'] = 'Ошибка перевода'
# Отправляем карточку слова
card_text = (
f"📝 <b>{word_data['word']}</b>\n"
f"🔊 [{word_data.get('transcription', '')}]\n\n"
f"{t(lang, 'add.translation_label')}: {word_data['translation']}\n"
f"{t(lang, 'add.category_label')}: {word_data.get('category', '')}\n"
f"{t(lang, 'add.level_label')}: {word_data.get('difficulty', 'A1')}"
f"{examples_text}\n\n"
f"{translations_text}"
f"{t(lang, 'add.confirm_question')}"
)
@@ -130,7 +144,7 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
ui_lang = user.language_interface if user else 'ru'
# Добавляем слово в базу
await VocabularyService.add_word(
new_word = await VocabularyService.add_word(
session,
user_id=user_id,
word_original=word_data["word"],
@@ -138,12 +152,20 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
source_lang=source_lang,
translation_lang=ui_lang,
transcription=word_data.get("transcription"),
examples={"examples": word_data.get("examples", [])},
category=word_data.get("category"),
difficulty_level=word_data.get("difficulty"),
source=WordSource.MANUAL
)
# Сохраняем переводы с контекстами в отдельную таблицу
translations = word_data.get("translations", [])
if translations:
await VocabularyService.add_translations_bulk(
session,
vocabulary_id=new_word.id,
translations=translations
)
# Получаем общее количество слов
words_count = await VocabularyService.get_words_count(session, user_id, learning_lang=user.learning_language)
lang = ui_lang or 'ru'