Files
filwordspl-backend/generate_daily_phrase.py
2026-09-20 11:45:58 +00:00

200 lines
7.7 KiB
Python
Raw Permalink 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.
"""
Автоматическая генерация фразы дня.
Запускается через cron в 03:00 UTC (= 06:00 Минск).
Что делает:
1. Проверяет, есть ли фраза на сегодня в daily_phrases
2. Если нет — генерирует через AI (MiniMax-M3)
3. Пишет в daily_phrases с phrase_date = CURRENT_DATE
Fallback: если AI упал — пишет запасную фразу.
"""
import asyncio
import asyncpg
import httpx
import json
import logging
import random
from datetime import datetime, timezone
from config import DB_CONFIG, TOGETHER_API_KEY
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
TOGETHER_API_URL = "https://api.together.xyz/v1/chat/completions"
TEACHER_MODEL = "MiniMaxAI/MiniMax-M3"
# Список тем для фразы дня
TOPICS_LIST = [
"приветствие и знакомство",
"семья и друзья",
"еда и напитки",
"покупки и магазины",
"транспорт и путешествия",
"работа и профессии",
"погода и времена года",
"дом и квартира",
"хобби и свободное время",
"здоровье и самочувствие",
"город и местность",
"ежедневная рутина",
]
# Fallback фраза, если AI упал
FALLBACK_PHRASE = "Lepiej późno niż wcale."
FALLBACK_TRANSLATIONS = {
"ru": "Лучше поздно, чем никогда.",
"uk": "Краще пізно, ніж ніколи.",
"en": "Better late than never."
}
FALLBACK_GRAMMAR = {
"pl": "Lepiej - przysłówek w stopniu wyższym. Późno - przysłówek.",
"ru": "Lepiej - наречие в сравнительной степени. Późno - наречие.",
"uk": "Lepiej - прислівник у вищому ступені. Późno - прислівник.",
"en": "Lepiej - adverb in comparative degree. Późno - adverb."
}
async def call_ai(system_prompt: str, user_prompt: str, max_tokens: int = 1000) -> str:
"""Вызывает AI и возвращает ответ."""
if not TOGETHER_API_KEY:
logger.error("TOGETHER_API_KEY not configured")
return ""
try:
async with httpx.AsyncClient(timeout=90.0) as client:
response = await client.post(
TOGETHER_API_URL,
headers={
"Authorization": f"Bearer {TOGETHER_API_KEY}",
"Content-Type": "application/json",
},
json={
"model": TEACHER_MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"max_tokens": max_tokens,
"temperature": 0.7,
},
)
if response.status_code != 200:
logger.error(f"AI API error: {response.status_code}, body={response.text[:200]}")
return ""
data = response.json()
return data["choices"][0]["message"]["content"].strip()
except Exception as e:
logger.error(f"AI call error: {e}")
return ""
async def generate_phrase() -> tuple:
"""
Генерирует фразу дня через AI.
Возвращает (phrase_pl, translations_dict, grammar_dict).
"""
selected_topic = random.choice(TOPICS_LIST)
system_prompt = f"""Ты — учитель польского языка. Создай фразу дня для изучения польского.
Тема фразы: {selected_topic}.
Фраза должна быть уровня A2-B1, полезной в повседневной жизни.
Верни ответ строго в таком формате (без markdown, просто текст):
PHRASE: [фраза на польском]
RU: [перевод на русский]
UK: [перевод на украинский]
EN: [перевод на английский]
PL_GRAMMAR: [грамматический разбор на польском, 1-2 предложения]
RU_GRAMMAR: [грамматический разбор на русском, 1-2 предложения]
UK_GRAMMAR: [грамматический разбор на украинском, 1-2 предложения]
EN_GRAMMAR: [грамматический разбор на английском, 1-2 предложения]"""
user_prompt = f"Создай фразу дня на тему: {selected_topic}."
ai_response = await call_ai(system_prompt, user_prompt, max_tokens=1000)
if not ai_response:
logger.warning("AI returned empty response, using fallback")
return FALLBACK_PHRASE, FALLBACK_TRANSLATIONS, FALLBACK_GRAMMAR
# Парсим ответ
phrase_pl = FALLBACK_PHRASE
translations = FALLBACK_TRANSLATIONS.copy()
grammar_breakdowns = FALLBACK_GRAMMAR.copy()
for line in ai_response.strip().split('\n'):
line = line.strip()
if line.startswith('PHRASE:'):
phrase_pl = line.replace('PHRASE:', '').strip()
elif line.startswith('RU:'):
translations["ru"] = line.replace('RU:', '').strip()
elif line.startswith('UK:'):
translations["uk"] = line.replace('UK:', '').strip()
elif line.startswith('EN:'):
translations["en"] = line.replace('EN:', '').strip()
elif line.startswith('PL_GRAMMAR:'):
grammar_breakdowns["pl"] = line.replace('PL_GRAMMAR:', '').strip()
elif line.startswith('RU_GRAMMAR:'):
grammar_breakdowns["ru"] = line.replace('RU_GRAMMAR:', '').strip()
elif line.startswith('UK_GRAMMAR:'):
grammar_breakdowns["uk"] = line.replace('UK_GRAMMAR:', '').strip()
elif line.startswith('EN_GRAMMAR:'):
grammar_breakdowns["en"] = line.replace('EN_GRAMMAR:', '').strip()
logger.info(f"Generated phrase for topic '{selected_topic}': {phrase_pl[:60]}...")
return phrase_pl, translations, grammar_breakdowns
async def main():
logger.info("=== Начало генерации фразы дня ===")
conn = await asyncpg.connect(**DB_CONFIG)
try:
today = datetime.now(timezone.utc).date()
# Проверяем, есть ли уже фраза на сегодня
existing = await conn.fetchrow("""
SELECT id FROM daily_phrases WHERE phrase_date = $1
""", today)
if existing:
logger.info(f"Phrase for {today} already exists, skipping")
return
# Генерируем новую фразу
logger.info(f"Generating phrase for {today}...")
phrase_pl, translations, grammar_breakdowns = await generate_phrase()
# Пишем в БД
await conn.execute("""
INSERT INTO daily_phrases (phrase_date, phrase_pl, translations, grammar_breakdowns)
VALUES ($1, $2, $3::jsonb, $4::jsonb)
ON CONFLICT (phrase_date) DO UPDATE SET
phrase_pl = EXCLUDED.phrase_pl,
translations = EXCLUDED.translations,
grammar_breakdowns = EXCLUDED.grammar_breakdowns
""", today, phrase_pl,
json.dumps(translations, ensure_ascii=False),
json.dumps(grammar_breakdowns, ensure_ascii=False))
logger.info(f"✅ Phrase for {today} saved: {phrase_pl}")
except Exception as e:
logger.error(f"Error: {e}")
finally:
await conn.close()
logger.info("=== Готово ===")
if __name__ == "__main__":
asyncio.run(main())