diff --git a/.DS_Store b/.DS_Store
index dc14a65..b9c8e34 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/bot/handlers/tasks.py b/bot/handlers/tasks.py
index bf87916..f2ca9b1 100644
--- a/bot/handlers/tasks.py
+++ b/bot/handlers/tasks.py
@@ -663,4 +663,51 @@ async def cmd_stats(message: Message):
else:
stats_text += t(lang, 'stats.hint_keep_practice')
- await message.answer(stats_text)
+ # Кнопка "Слово дня"
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(
+ text=t(lang, 'stats.word_of_day_btn'),
+ callback_data="stats_word_of_day"
+ )]
+ ])
+
+ await message.answer(stats_text, reply_markup=keyboard)
+
+
+@router.callback_query(F.data == "stats_word_of_day")
+async def stats_word_of_day(callback: CallbackQuery):
+ """Показать слово дня из статистики"""
+ await callback.answer()
+
+ from services.wordofday_service import wordofday_service
+ from bot.handlers.wordofday import format_word_of_day
+
+ async with async_session_maker() as session:
+ user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
+
+ if not user:
+ return
+
+ lang = get_user_lang(user)
+ learning_lang = user.learning_language or 'en'
+ level = get_user_level_for_language(user)
+
+ wod = await wordofday_service.get_word_of_day(
+ learning_lang=learning_lang,
+ level=level
+ )
+
+ if not wod:
+ await callback.message.answer(t(lang, 'wod.not_available'))
+ return
+
+ text = format_word_of_day(wod, lang)
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(
+ text=t(lang, 'wod.add_btn'),
+ callback_data=f"wod_add_{wod.id}"
+ )]
+ ])
+
+ await callback.message.answer(text, reply_markup=keyboard)
diff --git a/locales/en.json b/locales/en.json
index debffc3..b95dc94 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -187,7 +187,8 @@
"accuracy": "🎯 Accuracy: {n}%",
"hint_add_words": "Add words with /add to start learning!",
"hint_first_task": "Do your first task with /task!",
- "hint_keep_practice": "Keep practicing! 💪"
+ "hint_keep_practice": "Keep practicing! 💪",
+ "word_of_day_btn": "🌅 Word of the Day"
},
"reminder": {
"title": "⏰ Reminders",
diff --git a/locales/ja.json b/locales/ja.json
index 20412f9..6828e75 100644
--- a/locales/ja.json
+++ b/locales/ja.json
@@ -179,7 +179,8 @@
"accuracy": "🎯 正答率: {n}%",
"hint_add_words": "/add で単語を追加して学習を始めましょう!",
"hint_first_task": "/task で最初の課題をやってみましょう!",
- "hint_keep_practice": "練習を続けましょう! 💪"
+ "hint_keep_practice": "練習を続けましょう! 💪",
+ "word_of_day_btn": "🌅 今日の単語"
},
"reminder": {
"title": "⏰ リマインダー",
diff --git a/locales/ru.json b/locales/ru.json
index 3da6b08..24edbe5 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -264,7 +264,8 @@
"accuracy": "🎯 Точность: {n}%",
"hint_add_words": "Добавь слова командой /add чтобы начать обучение!",
"hint_first_task": "Выполни первое задание командой /task!",
- "hint_keep_practice": "Продолжай практиковаться! 💪"
+ "hint_keep_practice": "Продолжай практиковаться! 💪",
+ "word_of_day_btn": "🌅 Слово дня"
},
"level_test": {
"show_translation_btn": "👁️ Показать перевод вопроса",
diff --git a/services/ai_service.py b/services/ai_service.py
index cd9a84a..45f1bab 100644
--- a/services/ai_service.py
+++ b/services/ai_service.py
@@ -1371,74 +1371,76 @@ User: {user_message}
}
]
- async def generate_word_of_day(
+ async def generate_words_of_day_batch(
self,
- level: str,
- learning_lang: str = "en",
+ language: str,
+ levels: List[str],
translation_lang: str = "ru",
- excluded_words: List[str] = None,
- user_id: Optional[int] = None
- ) -> Optional[Dict]:
+ excluded_words: Dict[str, List[str]] = None
+ ) -> Optional[Dict[str, Dict]]:
"""
- Генерация слова дня.
+ Генерация слов дня для всех уровней одного языка за один запрос.
Args:
- level: Уровень пользователя (A1-C2 или N5-N1)
- learning_lang: Язык изучения
+ language: Язык изучения (en/ja)
+ levels: Список уровней (A1-C2 или N5-N1)
translation_lang: Язык перевода
- excluded_words: Список слов для исключения (уже были)
- user_id: ID пользователя для выбора модели
+ excluded_words: Dict {level: [excluded_words]} для исключения
Returns:
- Dict с полями: word, transcription, translation, examples, synonyms, etymology
+ Dict {level: word_data} или None при ошибке
"""
- language_names = {
- "en": "английский",
- "ja": "японский"
- }
- language_name = language_names.get(learning_lang, "английский")
+ language_names = {"en": "английский", "ja": "японский"}
+ language_name = language_names.get(language, "английский")
- translation_names = {
- "ru": "русский",
- "en": "английский",
- "ja": "японский"
- }
+ translation_names = {"ru": "русский", "en": "английский", "ja": "японский"}
translation_name = translation_names.get(translation_lang, "русский")
- excluded_str = ""
+ # Формируем список исключений по уровням
+ excluded_info = ""
if excluded_words:
- excluded_str = f"\n\nНЕ используй эти слова (уже были): {', '.join(excluded_words[:20])}"
+ excluded_parts = []
+ for level, words in excluded_words.items():
+ if words:
+ excluded_parts.append(f"- {level}: {', '.join(words[:15])}")
+ if excluded_parts:
+ excluded_info = "\n\nНЕ используй эти слова (уже были недавно):\n" + "\n".join(excluded_parts)
- prompt = f"""Сгенерируй интересное "слово дня" для изучающего {language_name} язык на уровне {level}.
+ levels_str = ", ".join(levels)
-Требования:
+ prompt = f"""Сгенерируй "слово дня" для изучающих {language_name} язык на каждом из уровней: {levels_str}.
+
+Требования для каждого слова:
- Слово должно быть полезным и интересным
-- Подходящее для уровня {level}
-- НЕ слишком простое и НЕ слишком сложное
-- Желательно с интересной этимологией или фактом{excluded_str}
+- Строго соответствовать указанному уровню сложности
+- Желательно с интересной этимологией или фактом
+- Все слова должны быть РАЗНЫМИ{excluded_info}
-Верни JSON:
+Верни JSON объект, где ключи - уровни ({levels_str}):
{{
- "word": "слово на {language_name}",
- "transcription": "транскрипция (IPA для английского, хирагана для японского)",
- "translation": "перевод на {translation_name}",
- "examples": [
- {{"sentence": "пример предложения", "translation": "перевод примера"}},
- {{"sentence": "второй пример", "translation": "перевод"}}
- ],
- "synonyms": "синоним1, синоним2, синоним3",
- "etymology": "краткий интересный факт о слове или его происхождении (1-2 предложения)"
+ "{levels[0]}": {{
+ "word": "слово на {language_name}",
+ "transcription": "транскрипция (IPA для английского, хирагана для японского)",
+ "translation": "перевод на {translation_name}",
+ "examples": [
+ {{"sentence": "пример предложения", "translation": "перевод примера"}},
+ {{"sentence": "второй пример", "translation": "перевод"}}
+ ],
+ "synonyms": "синоним1, синоним2",
+ "etymology": "краткий интересный факт (1-2 предложения)"
+ }},
+ ... (для каждого уровня)
}}"""
try:
- logger.info(f"[AI Request] generate_word_of_day: level='{level}', lang='{learning_lang}'")
+ logger.info(f"[AI Request] generate_words_of_day_batch: lang='{language}', levels={levels}")
messages = [
- {"role": "system", "content": "Ты - опытный лингвист, который подбирает интересные слова для изучения."},
+ {"role": "system", "content": "Ты - опытный лингвист. Отвечай только валидным JSON без markdown."},
{"role": "user", "content": prompt}
]
- model_name, provider = await self._get_active_model(user_id)
+ model_name, provider = await self._get_active_model(None)
if provider == AIProvider.google:
response_data = await self._make_google_request(messages, temperature=0.8, model=model_name)
@@ -1449,11 +1451,11 @@ User: {user_message}
content = self._strip_markdown_code_block(content)
result = json.loads(content)
- logger.info(f"[AI Response] generate_word_of_day: word='{result.get('word', 'N/A')}'")
+ logger.info(f"[AI Response] generate_words_of_day_batch: generated {len(result)} words")
return result
except Exception as e:
- logger.error(f"[AI Error] generate_word_of_day: {type(e).__name__}: {str(e)}")
+ logger.error(f"[AI Error] generate_words_of_day_batch: {type(e).__name__}: {str(e)}")
return None
async def generate_mini_story(
diff --git a/services/wordofday_service.py b/services/wordofday_service.py
index 1df63ab..ef58281 100644
--- a/services/wordofday_service.py
+++ b/services/wordofday_service.py
@@ -27,6 +27,7 @@ class WordOfDayService:
"""
Генерация слов дня для всех языков и уровней.
Вызывается в 00:00 UTC.
+ Использует batch-генерацию: 1 запрос на язык вместо 1 запроса на уровень.
Returns:
Dict с количеством сгенерированных слов по языкам
@@ -38,30 +39,44 @@ class WordOfDayService:
for lang in LEARNING_LANGUAGES:
levels = JLPT_LEVELS if lang in JLPT_LANGUAGES else CEFR_LEVELS
+ # Определяем какие уровни ещё не сгенерированы
+ levels_to_generate = []
for level in levels:
- try:
- # Проверяем, не сгенерировано ли уже
- existing = await self._get_word_for_date(
- session, today, lang, level
- )
- if existing:
- logger.debug(
- f"Слово дня уже существует: {lang}/{level}"
- )
- continue
+ existing = await self._get_word_for_date(session, today, lang, level)
+ if not existing:
+ levels_to_generate.append(level)
+ else:
+ logger.debug(f"Слово дня уже существует: {lang}/{level}")
- # Получаем список недавних слов для исключения
- excluded = await self._get_recent_words(session, lang, level, days=30)
+ if not levels_to_generate:
+ logger.info(f"Все слова для {lang} уже сгенерированы")
+ continue
- # Генерируем слово
- word_data = await ai_service.generate_word_of_day(
- level=level,
- learning_lang=lang,
- translation_lang="ru", # Базовый перевод на русский
- excluded_words=excluded
- )
+ # Собираем исключения для каждого уровня
+ excluded_words = {}
+ for level in levels_to_generate:
+ excluded_words[level] = await self._get_recent_words(
+ session, lang, level, days=30
+ )
- if word_data:
+ # Генерируем все слова одним запросом
+ try:
+ batch_result = await ai_service.generate_words_of_day_batch(
+ language=lang,
+ levels=levels_to_generate,
+ translation_lang="ru",
+ excluded_words=excluded_words
+ )
+
+ if not batch_result:
+ results["errors"] += len(levels_to_generate)
+ logger.error(f"Не удалось сгенерировать слова для {lang}")
+ continue
+
+ # Сохраняем каждое слово
+ for level in levels_to_generate:
+ word_data = batch_result.get(level)
+ if word_data and word_data.get("word"):
word_of_day = WordOfDay(
word=word_data.get("word", ""),
transcription=word_data.get("transcription"),
@@ -74,7 +89,6 @@ class WordOfDayService:
date=today
)
session.add(word_of_day)
- await session.commit()
results[lang] += 1
logger.info(
f"Сгенерировано слово дня: {word_data.get('word')} "
@@ -82,15 +96,13 @@ class WordOfDayService:
)
else:
results["errors"] += 1
- logger.warning(
- f"Не удалось сгенерировать слово для {lang}/{level}"
- )
+ logger.warning(f"Нет данных для {lang}/{level} в ответе AI")
- except Exception as e:
- results["errors"] += 1
- logger.error(
- f"Ошибка генерации слова для {lang}/{level}: {e}"
- )
+ await session.commit()
+
+ except Exception as e:
+ results["errors"] += len(levels_to_generate)
+ logger.error(f"Ошибка batch-генерации для {lang}: {e}")
total = results["en"] + results["ja"]
logger.info(