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

@@ -1,7 +1,7 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import Vocabulary, WordSource, LanguageLevel
from typing import List, Optional
from database.models import Vocabulary, WordSource, LanguageLevel, WordTranslation
from typing import List, Optional, Dict
import re
@@ -176,3 +176,183 @@ class VocabularyService:
.where(Vocabulary.word_original == word.lower())
)
return result.scalar_one_or_none()
@staticmethod
async def get_all_user_word_strings(
session: AsyncSession,
user_id: int,
learning_lang: Optional[str] = None
) -> List[str]:
"""
Получить список всех слов пользователя (только строки)
Args:
session: Сессия базы данных
user_id: ID пользователя
learning_lang: Язык изучения для фильтрации
Returns:
Список строк — оригинальных слов
"""
result = await session.execute(
select(Vocabulary)
.where(Vocabulary.user_id == user_id)
)
words = list(result.scalars().all())
words = VocabularyService._filter_by_learning_lang(words, learning_lang)
return [w.word_original.lower() for w in words]
# === Методы для работы с переводами ===
@staticmethod
async def add_translation(
session: AsyncSession,
vocabulary_id: int,
translation: str,
context: Optional[str] = None,
context_translation: Optional[str] = None,
is_primary: bool = False
) -> WordTranslation:
"""
Добавить перевод к слову
Args:
session: Сессия базы данных
vocabulary_id: ID слова в словаре
translation: Перевод
context: Пример предложения на языке изучения
context_translation: Перевод примера
is_primary: Является ли основным переводом
Returns:
Созданный объект перевода
"""
# Если это основной перевод, снимаем флаг с других
if is_primary:
result = await session.execute(
select(WordTranslation)
.where(WordTranslation.vocabulary_id == vocabulary_id)
.where(WordTranslation.is_primary == True)
)
for existing in result.scalars().all():
existing.is_primary = False
new_translation = WordTranslation(
vocabulary_id=vocabulary_id,
translation=translation,
context=context,
context_translation=context_translation,
is_primary=is_primary
)
session.add(new_translation)
await session.commit()
await session.refresh(new_translation)
return new_translation
@staticmethod
async def add_translations_bulk(
session: AsyncSession,
vocabulary_id: int,
translations: List[Dict]
) -> List[WordTranslation]:
"""
Добавить несколько переводов к слову
Args:
session: Сессия базы данных
vocabulary_id: ID слова
translations: Список словарей с переводами
[{"translation": "...", "context": "...", "context_translation": "...", "is_primary": bool}]
Returns:
Список созданных переводов
"""
created = []
for i, tr_data in enumerate(translations):
new_translation = WordTranslation(
vocabulary_id=vocabulary_id,
translation=tr_data.get('translation', ''),
context=tr_data.get('context'),
context_translation=tr_data.get('context_translation'),
is_primary=tr_data.get('is_primary', i == 0) # Первый по умолчанию основной
)
session.add(new_translation)
created.append(new_translation)
await session.commit()
for tr in created:
await session.refresh(tr)
return created
@staticmethod
async def get_word_translations(
session: AsyncSession,
vocabulary_id: int
) -> List[WordTranslation]:
"""
Получить все переводы слова
Args:
session: Сессия базы данных
vocabulary_id: ID слова
Returns:
Список переводов
"""
result = await session.execute(
select(WordTranslation)
.where(WordTranslation.vocabulary_id == vocabulary_id)
.order_by(WordTranslation.is_primary.desc(), WordTranslation.created_at)
)
return list(result.scalars().all())
@staticmethod
async def get_primary_translation(
session: AsyncSession,
vocabulary_id: int
) -> Optional[WordTranslation]:
"""
Получить основной перевод слова
Args:
session: Сессия базы данных
vocabulary_id: ID слова
Returns:
Основной перевод или None
"""
result = await session.execute(
select(WordTranslation)
.where(WordTranslation.vocabulary_id == vocabulary_id)
.where(WordTranslation.is_primary == True)
)
return result.scalar_one_or_none()
@staticmethod
async def delete_translation(
session: AsyncSession,
translation_id: int
) -> bool:
"""
Удалить перевод
Args:
session: Сессия базы данных
translation_id: ID перевода
Returns:
True если удалено, False если не найдено
"""
result = await session.execute(
select(WordTranslation).where(WordTranslation.id == translation_id)
)
translation = result.scalar_one_or_none()
if translation:
await session.delete(translation)
await session.commit()
return True
return False