Files
tg_bot_language/services/wordofday_service.py
mamonov.ep badad0a529 feat: batch-генерация слов дня, кнопка "Слово дня" в статистике
- Оптимизирована генерация слов дня: 2 запроса к AI вместо 11
- Добавлена кнопка "Слово дня" в /stats для быстрого доступа
- Локализация для ru/en/ja

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 11:00:30 +03:00

240 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Сервис генерации слова дня"""
import logging
from datetime import datetime, date
from typing import Optional, Dict, List
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession
from database.db import async_session_maker
from database.models import WordOfDay, LanguageLevel, JLPTLevel, JLPT_LANGUAGES
from services.ai_service import ai_service
logger = logging.getLogger(__name__)
# Уровни для каждого языка
CEFR_LEVELS = [level.value for level in LanguageLevel] # A1-C2
JLPT_LEVELS = [level.value for level in JLPTLevel] # N5-N1
# Языки для генерации
LEARNING_LANGUAGES = ["en", "ja"]
class WordOfDayService:
"""Сервис для генерации и получения слова дня"""
async def generate_all_words_for_today(self) -> Dict[str, int]:
"""
Генерация слов дня для всех языков и уровней.
Вызывается в 00:00 UTC.
Использует batch-генерацию: 1 запрос на язык вместо 1 запроса на уровень.
Returns:
Dict с количеством сгенерированных слов по языкам
"""
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
results = {"en": 0, "ja": 0, "errors": 0}
async with async_session_maker() as session:
for lang in LEARNING_LANGUAGES:
levels = JLPT_LEVELS if lang in JLPT_LANGUAGES else CEFR_LEVELS
# Определяем какие уровни ещё не сгенерированы
levels_to_generate = []
for level in levels:
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}")
if not levels_to_generate:
logger.info(f"Все слова для {lang} уже сгенерированы")
continue
# Собираем исключения для каждого уровня
excluded_words = {}
for level in levels_to_generate:
excluded_words[level] = await self._get_recent_words(
session, lang, level, days=30
)
# Генерируем все слова одним запросом
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"),
translation=word_data.get("translation", ""),
examples=word_data.get("examples"),
synonyms=word_data.get("synonyms"),
etymology=word_data.get("etymology"),
learning_lang=lang,
level=level,
date=today
)
session.add(word_of_day)
results[lang] += 1
logger.info(
f"Сгенерировано слово дня: {word_data.get('word')} "
f"({lang}/{level})"
)
else:
results["errors"] += 1
logger.warning(f"Нет данных для {lang}/{level} в ответе AI")
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(
f"Генерация слов дня завершена: всего={total}, "
f"en={results['en']}, ja={results['ja']}, ошибок={results['errors']}"
)
return results
async def get_word_of_day(
self,
learning_lang: str,
level: str,
target_date: Optional[datetime] = None
) -> Optional[WordOfDay]:
"""
Получить слово дня для языка и уровня.
Args:
learning_lang: Язык изучения (en/ja)
level: Уровень (A1-C2 или N5-N1)
target_date: Дата (по умолчанию сегодня)
Returns:
WordOfDay или None
"""
if target_date is None:
target_date = datetime.utcnow().replace(
hour=0, minute=0, second=0, microsecond=0
)
else:
target_date = target_date.replace(
hour=0, minute=0, second=0, microsecond=0
)
async with async_session_maker() as session:
return await self._get_word_for_date(
session, target_date, learning_lang, level
)
async def _get_word_for_date(
self,
session: AsyncSession,
target_date: datetime,
learning_lang: str,
level: str
) -> Optional[WordOfDay]:
"""Получить слово из БД для конкретной даты"""
result = await session.execute(
select(WordOfDay).where(
and_(
WordOfDay.date == target_date,
WordOfDay.learning_lang == learning_lang,
WordOfDay.level == level
)
)
)
return result.scalar_one_or_none()
async def _get_recent_words(
self,
session: AsyncSession,
learning_lang: str,
level: str,
days: int = 30
) -> List[str]:
"""Получить список недавних слов для исключения"""
from datetime import timedelta
cutoff_date = datetime.utcnow() - timedelta(days=days)
result = await session.execute(
select(WordOfDay.word).where(
and_(
WordOfDay.learning_lang == learning_lang,
WordOfDay.level == level,
WordOfDay.date >= cutoff_date
)
)
)
return [row[0] for row in result.fetchall()]
async def format_word_for_user(
self,
word: WordOfDay,
translation_lang: str = "ru",
ui_lang: str = None
) -> str:
"""
Форматировать слово дня для отображения пользователю.
Args:
word: Объект WordOfDay
translation_lang: Язык перевода для пользователя
ui_lang: Язык интерфейса (для локализации заголовков)
Returns:
Отформатированная строка
"""
from utils.i18n import t
lang = ui_lang or translation_lang or "ru"
lines = []
# Заголовок со словом
if word.transcription:
lines.append(f"📚 <b>{word.word}</b> [{word.transcription}]")
else:
lines.append(f"📚 <b>{word.word}</b>")
# Перевод
lines.append(f"📝 {word.translation}")
# Синонимы
if word.synonyms:
lines.append(f"\n🔄 <b>{t(lang, 'wod.synonyms')}:</b> {word.synonyms}")
# Примеры
if word.examples:
lines.append(f"\n📖 <b>{t(lang, 'wod.examples')}:</b>")
for i, example in enumerate(word.examples[:3], 1):
sentence = example.get("sentence", "")
translation = example.get("translation", "")
lines.append(f" {i}. {sentence}")
if translation:
lines.append(f" <i>{translation}</i>")
# Этимология/интересный факт
if word.etymology:
lines.append(f"\n💡 {word.etymology}")
return "\n".join(lines)
# Глобальный экземпляр сервиса
wordofday_service = WordOfDayService()