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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -93,6 +93,19 @@ class Vocabulary(Base):
|
||||
notes: Mapped[Optional[str]] = mapped_column(String(500)) # Заметки пользователя
|
||||
|
||||
|
||||
class WordTranslation(Base):
|
||||
"""Модель перевода слова с контекстом"""
|
||||
__tablename__ = "word_translations"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
vocabulary_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
translation: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
context: Mapped[Optional[str]] = mapped_column(String(500)) # Пример предложения
|
||||
context_translation: Mapped[Optional[str]] = mapped_column(String(500)) # Перевод примера
|
||||
is_primary: Mapped[bool] = mapped_column(Boolean, default=False) # Основной перевод
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class Task(Base):
|
||||
"""Модель задания"""
|
||||
__tablename__ = "tasks"
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"prompt": "Send the word you want to add:\nFor example: <code>/add elephant</code>\n\nOr just send the word without a command!",
|
||||
"searching": "⏳ Looking up translation and examples...",
|
||||
"examples_header": "<b>Examples:</b>",
|
||||
"translations_header": "<b>Translations:</b>",
|
||||
"translation_label": "Translation",
|
||||
"category_label": "Category",
|
||||
"level_label": "Level",
|
||||
@@ -132,6 +133,7 @@
|
||||
"add_word_btn": "➕ Add word",
|
||||
"word_added": "✅ Word '{word}' added to vocabulary!",
|
||||
"word_already_exists": "Word '{word}' is already in vocabulary",
|
||||
"example_label": "Example",
|
||||
"cancelled": "Cancelled. You can return to tasks with /task.",
|
||||
"finish_title": "{emoji} <b>Task finished!</b>",
|
||||
"correct_of": "Correct answers: <b>{correct}</b> of {total}",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"prompt": "追加したい単語を送ってください:\n例: <code>/add elephant</code>\n\nコマンドなしで単語だけ送ってもOKです!",
|
||||
"searching": "⏳ 翻訳と例を検索中...",
|
||||
"examples_header": "<b>例文:</b>",
|
||||
"translations_header": "<b>翻訳:</b>",
|
||||
"translation_label": "翻訳",
|
||||
"category_label": "カテゴリー",
|
||||
"level_label": "レベル",
|
||||
@@ -124,6 +125,7 @@
|
||||
"add_word_btn": "➕ 単語を追加",
|
||||
"word_added": "✅ 単語 '{word}' を単語帳に追加しました!",
|
||||
"word_already_exists": "単語 '{word}' はすでに単語帳にあります",
|
||||
"example_label": "例文",
|
||||
"cancelled": "キャンセルしました。/task で課題に戻れます。",
|
||||
"finish_title": "{emoji} <b>課題が終了しました!</b>",
|
||||
"correct_of": "正解数: <b>{correct}</b> / {total}",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"prompt": "Отправь слово, которое хочешь добавить:\nНапример: <code>/add elephant</code>\n\nИли просто отправь слово без команды!",
|
||||
"searching": "⏳ Ищу перевод и примеры...",
|
||||
"examples_header": "<b>Примеры:</b>",
|
||||
"translations_header": "<b>Переводы:</b>",
|
||||
"translation_label": "Перевод",
|
||||
"category_label": "Категория",
|
||||
"level_label": "Уровень",
|
||||
@@ -132,6 +133,7 @@
|
||||
"add_word_btn": "➕ Добавить слово",
|
||||
"word_added": "✅ Слово '{word}' добавлено в словарь!",
|
||||
"word_already_exists": "Слово '{word}' уже в словаре",
|
||||
"example_label": "Пример",
|
||||
"cancelled": "Отменено. Можешь вернуться к заданиям командой /task.",
|
||||
"finish_title": "{emoji} <b>Задание завершено!</b>",
|
||||
"correct_of": "Правильных ответов: <b>{correct}</b> из {total}",
|
||||
|
||||
37
migrations/versions/20251206_add_word_translations.py
Normal file
37
migrations/versions/20251206_add_word_translations.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Add word_translations table for multiple translations with context
|
||||
|
||||
Revision ID: 20251206_word_translations
|
||||
Revises: 20251205_wordsource_ai_task
|
||||
Create Date: 2025-12-06
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '20251206_word_translations'
|
||||
down_revision: Union[str, None] = '20251205_wordsource_ai_task'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'word_translations',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vocabulary_id', sa.Integer(), nullable=False),
|
||||
sa.Column('translation', sa.String(255), nullable=False),
|
||||
sa.Column('context', sa.String(500), nullable=True),
|
||||
sa.Column('context_translation', sa.String(500), nullable=True),
|
||||
sa.Column('is_primary', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('ix_word_translations_vocabulary_id', 'word_translations', ['vocabulary_id'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('ix_word_translations_vocabulary_id', table_name='word_translations')
|
||||
op.drop_table('word_translations')
|
||||
@@ -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)}")
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import List, Dict, Optional
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database.models import Task, Vocabulary
|
||||
from database.models import Task, Vocabulary, WordTranslation
|
||||
from services.ai_service import ai_service
|
||||
|
||||
|
||||
@@ -107,8 +107,54 @@ class TaskService:
|
||||
|
||||
tasks = []
|
||||
for word in selected_words:
|
||||
# Получаем переводы из таблицы WordTranslation
|
||||
translations_result = await session.execute(
|
||||
select(WordTranslation)
|
||||
.where(WordTranslation.vocabulary_id == word.id)
|
||||
.order_by(WordTranslation.is_primary.desc())
|
||||
)
|
||||
translations = list(translations_result.scalars().all())
|
||||
|
||||
# Случайно выбираем тип задания
|
||||
task_type = random.choice(['translate', 'fill_in'])
|
||||
# Если есть переводы с контекстом, добавляем тип 'context_translate'
|
||||
task_types = ['translate', 'fill_in']
|
||||
if translations and any(tr.context for tr in translations):
|
||||
task_types.append('context_translate')
|
||||
|
||||
task_type = random.choice(task_types)
|
||||
|
||||
if task_type == 'context_translate' and translations:
|
||||
# Задание на перевод по контексту
|
||||
# Выбираем случайный перевод с контекстом
|
||||
translations_with_context = [tr for tr in translations if tr.context]
|
||||
if translations_with_context:
|
||||
selected_tr = random.choice(translations_with_context)
|
||||
|
||||
# Локализация фразы
|
||||
if translation_lang == 'en':
|
||||
prompt = "Translate the highlighted word in context:"
|
||||
elif translation_lang == 'ja':
|
||||
prompt = "文脈に合った翻訳を入力してください:"
|
||||
else:
|
||||
prompt = "Переведи выделенное слово в контексте:"
|
||||
|
||||
task = {
|
||||
'type': 'context_translate',
|
||||
'word_id': word.id,
|
||||
'translation_id': selected_tr.id,
|
||||
'question': (
|
||||
f"{prompt}\n\n"
|
||||
f"<i>«{selected_tr.context}»</i>\n\n"
|
||||
f"<b>{word.word_original}</b> = ?"
|
||||
),
|
||||
'word': word.word_original,
|
||||
'correct_answer': selected_tr.translation,
|
||||
'transcription': word.transcription,
|
||||
'context': selected_tr.context,
|
||||
'context_translation': selected_tr.context_translation
|
||||
}
|
||||
tasks.append(task)
|
||||
continue
|
||||
|
||||
if task_type == 'translate':
|
||||
# Задание на перевод между языком обучения и языком перевода
|
||||
@@ -122,21 +168,31 @@ class TaskService:
|
||||
else:
|
||||
prompt = "Переведи слово:"
|
||||
|
||||
# Определяем правильный ответ - берём из таблицы переводов если есть
|
||||
correct_translation = word.word_translation
|
||||
if translations:
|
||||
# Берём основной перевод или первый
|
||||
primary = next((tr for tr in translations if tr.is_primary), translations[0] if translations else None)
|
||||
if primary:
|
||||
correct_translation = primary.translation
|
||||
|
||||
if direction == 'learn_to_tr':
|
||||
task = {
|
||||
'type': f'translate_to_{translation_lang}',
|
||||
'word_id': word.id,
|
||||
'question': f"{prompt} <b>{word.word_original}</b>",
|
||||
'word': word.word_original,
|
||||
'correct_answer': word.word_translation,
|
||||
'transcription': word.transcription
|
||||
'correct_answer': correct_translation,
|
||||
'transcription': word.transcription,
|
||||
# Все допустимые ответы для проверки
|
||||
'all_translations': [tr.translation for tr in translations] if translations else [correct_translation]
|
||||
}
|
||||
else:
|
||||
task = {
|
||||
'type': f'translate_to_{learning_lang}',
|
||||
'word_id': word.id,
|
||||
'question': f"{prompt} <b>{word.word_translation}</b>",
|
||||
'word': word.word_translation,
|
||||
'question': f"{prompt} <b>{correct_translation}</b>",
|
||||
'word': correct_translation,
|
||||
'correct_answer': word.word_original,
|
||||
'transcription': word.transcription
|
||||
}
|
||||
@@ -285,3 +341,34 @@ class TaskService:
|
||||
'correct_tasks': correct_tasks,
|
||||
'accuracy': accuracy
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_correctly_answered_words(
|
||||
session: AsyncSession,
|
||||
user_id: int
|
||||
) -> List[str]:
|
||||
"""
|
||||
Получить список слов, на которые пользователь правильно ответил в заданиях
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
|
||||
Returns:
|
||||
Список слов (строки) с правильными ответами
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(Task)
|
||||
.where(Task.user_id == user_id)
|
||||
.where(Task.is_correct == True)
|
||||
)
|
||||
tasks = list(result.scalars().all())
|
||||
|
||||
words = []
|
||||
for task in tasks:
|
||||
if task.content and isinstance(task.content, dict):
|
||||
word = task.content.get('word')
|
||||
if word:
|
||||
words.append(word.lower())
|
||||
|
||||
return list(set(words))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user