- Добавлены мини-истории для чтения с выбором жанра и вопросами - Кнопка показа/скрытия перевода истории - Количество вопросов берётся из настроек пользователя - Слово дня генерируется глобально в 00:00 UTC - Кнопка "Практика" открывает меню выбора режима - Убран автоматический create_all при запуске (только миграции) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
228 lines
8.5 KiB
Python
228 lines
8.5 KiB
Python
"""Сервис генерации слова дня"""
|
||
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.
|
||
|
||
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
|
||
|
||
for level in levels:
|
||
try:
|
||
# Проверяем, не сгенерировано ли уже
|
||
existing = await self._get_word_for_date(
|
||
session, today, lang, level
|
||
)
|
||
if existing:
|
||
logger.debug(
|
||
f"Слово дня уже существует: {lang}/{level}"
|
||
)
|
||
continue
|
||
|
||
# Получаем список недавних слов для исключения
|
||
excluded = await self._get_recent_words(session, lang, level, days=30)
|
||
|
||
# Генерируем слово
|
||
word_data = await ai_service.generate_word_of_day(
|
||
level=level,
|
||
learning_lang=lang,
|
||
translation_lang="ru", # Базовый перевод на русский
|
||
excluded_words=excluded
|
||
)
|
||
|
||
if word_data:
|
||
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)
|
||
await session.commit()
|
||
results[lang] += 1
|
||
logger.info(
|
||
f"Сгенерировано слово дня: {word_data.get('word')} "
|
||
f"({lang}/{level})"
|
||
)
|
||
else:
|
||
results["errors"] += 1
|
||
logger.warning(
|
||
f"Не удалось сгенерировать слово для {lang}/{level}"
|
||
)
|
||
|
||
except Exception as e:
|
||
results["errors"] += 1
|
||
logger.error(
|
||
f"Ошибка генерации слова для {lang}/{level}: {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()
|