metodika_core / app /main.py
zenivan's picture
...
9479eca
Raw
History Blame Contribute Delete
20.1 kB
import os
import logging
import tempfile
from datetime import datetime
from dotenv import load_dotenv
from openai import OpenAI
import gradio as gr
from docx import Document
import requests
from typing import Optional
import random
from app.quotes import quotes
from app.drawings import drawings
from app.knowledge_base.textbooks import TEXTBOOKS
# --- Настройка ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
load_dotenv()
api_key = os.getenv("API_KEY_openai")
client = OpenAI(api_key=api_key) if api_key else None
VS_ID = os.getenv("VECTOR_STORE_ID", "") # vector store for file_search if needed
# --- Утилиты ---
def upload_to_catbox(file_bytes: bytes) -> str:
"""Загружает файл на catbox.moe и возвращает URL"""
try:
response = requests.post(
"https://catbox.moe/user/api.php",
files={"fileToUpload": file_bytes},
data={"reqtype": "fileupload"},
timeout=10
)
response.raise_for_status()
return response.text.strip()
except Exception as e:
logging.error(f"Ошибка загрузки изображения: {e}")
raise gr.Error("Не удалось загрузить изображение")
def generate_docx(text: str) -> str:
"""Сохраняет текст в .docx и возвращает путь"""
doc = Document()
for line in text.split("\n"):
doc.add_paragraph(line)
tmp = tempfile.gettempdir()
path = os.path.join(tmp, f"lesson_plan_{datetime.now().strftime('%Y%m%d%H%M%S')}.docx")
doc.save(path)
return path
# названия textbooks для автозаполнения поля Учебник
import json
## Сохраняем отзыв через POST в google-таблицу
GOOGLE_SHEET_URL = os.getenv("FEEDBACK_GS_URL")
def save_feedback(comment, rate):
payload = {
"comment": comment,
"rate": rate
}
try:
requests.post(GOOGLE_SHEET_URL, json=payload, timeout=3)
except Exception as e:
print(f"Ошибка при отправке отзыва: {e}")
### ОСНОВНАЯ ФУНКЦИЯ ГЕНЕРАЦИИ
def generate_lesson_plan(
image_path: Optional[str],
textbook: str,
cefr: str,
topic: str,
goal: str,
format_type: str,
num_students: int,
age: str,
adults: bool,
level_match: int,
duration: int,
inventory: str,
extra_info: str,
methodology: str,
target_language: str,
hw_required: bool,
web_search: bool
# repetition: bool,
# application: bool,
# analysis: bool,
# creativity: bool
) -> str:
# Валидация API клиента
if not client:
raise gr.Error("API ключ не настроен")
# Проверка наличия изображения
if not image_path:
return "❗ Загрузите фото страницы учебника для генерации урока"
# Загружаем изображение и получаем URL
try:
with open(image_path, "rb") as f:
image_url = upload_to_catbox(f.read())
except Exception as e:
logging.error(f"Image upload error: {e}")
raise gr.Error("Не удалось загрузить изображение")
# Собираем текстовый prompt
lesson_params = {
'methodology': methodology,
'target_language': target_language, # для PPP
'textbook': textbook,
'cefr': cefr,
'topic': topic,
'goal': goal,
'num_students': num_students,
'age': 'Взрослые' if adults else age,
'level_match': level_match,
'duration': duration,
'inventory': inventory,
'hw_required': hw_required,
'extra_info': extra_info
}
# Получаем готовый промпт
from app.prompt_builder.prompt import build_prompt
full_prompt = build_prompt(lesson_params)
# Собираем входные данные согласно API
input_content = [
{
"type": "input_text",
"text": full_prompt
},
{
"type": "input_image",
"image_url": image_url,
"detail": "high"
}
]
# Опции инструментов
tools = []
tool_choice = None
if web_search:
tools.append({
"type": "web_search_preview",
"search_context_size": "medium",
"user_location": {"type": "approximate", "country": "RU"}
})
tool_choice = {"type": "web_search_preview"}
# Вызов LLM
try:
response = client.responses.create(
input=[{"role": "user", "content": input_content}],
model=os.getenv("OPENAI_MODEL"), # gpt-4o-mini gpt-4.1
tools=tools or None,
tool_choice=tool_choice,
max_output_tokens=8192,
reasoning= {"effort":"medium"},
stream=False
)
return response.output_text
except Exception as e:
logging.error(f"Generation error: {e}")
raise gr.Error(f"Ошибка генерации: {e}")
########## ИНТЕРФЕЙС
# Случайный рисунок в блокноте
drawing = random.choice(drawings)
# Текст с подсказкой и рисунком в блокноте
hint_text = f"""Здесь появится план занятия — заполните вводные и нажмите кнопку **Создать план**<br>
Создание может занять до 1 минуты
<pre>
{drawing}
</pre>
"""
#ТЕМА И СТИЛИ
### css привязывать именно так
#theme='earneleh/paris'
theme = gr.themes.Base(
secondary_hue="rose",
neutral_hue="stone",
).set(
body_background_fill='*primary_50',
body_text_color='*primary_900',
body_text_size='*text_lg',
body_text_color_subdued='*primary_700',
body_text_weight='600',
)
css_path = os.path.join(os.path.dirname(__file__), "styles.css")
# ИНТЕРФЕЙС
with gr.Blocks(theme=theme, css_paths=css_path) as app:
advanced_settings_visible = gr.State(value=False) # Импортируем gr.State для хранения состояния
feedback_visible = gr.State(False) # Хранит, открыт ли блок отзыва
gr.Markdown("# План урока английского языка", elem_classes=["main-title"])
quote_box = gr.Markdown(random.choice(quotes), elem_classes=["quote-block"])
with gr.Row():
with gr.Column(elem_classes=["left-col"], scale=1):
image = gr.Image(
label="Фото страницы учебника*",
type="filepath",
height=0, # Автоматическая высота
container=False # Не растягивать контейнер
)
# Блок 1: Учебник
with gr.Column(variant="panel"):
gr.Markdown("### 📚 Учебник", elem_classes=["block-title"])
textbook = gr.Textbox(label="Название учебника", placeholder="напр. English File Beginner", elem_id="textbook_input")
cefr = gr.Dropdown(label="Уровень", choices=["", "A1", "A2", "B1", "B2", "C1", "C2"],
value="", info="Выберите уровень")
# Блок 2: Класс
with gr.Column(variant="panel"):
gr.Markdown("### 👥 Класс", elem_classes=["block-title"])
format_type = gr.Radio(label="Формат занятия", choices=["Индивидуальное", "Групповое"],
value="Групповое")
with gr.Group(visible=True) as group_settings:
num_students = gr.Slider(label="Количество учеников", minimum=1, maximum=40,
value=10, step=1, info="Для групповых занятий")
with gr.Group() as age_group:
age = gr.Textbox(label="Возраст", placeholder="напр. 10–11", interactive=True)
adults = gr.Checkbox(label="Взрослые")
level_match = gr.Dropdown(label = "Уровень учеников относительно учебника",
choices = ["ниже", "на уровне", "выше", "mixed"], value = "на уровне")
# Блок 3: Занятие
with gr.Column(variant="panel"):
gr.Markdown("### 📄 Занятие", elem_classes=["block-title"])
topic = gr.Textbox(label="Тема", placeholder="напр. Daily routines")
goal = gr.Textbox(label="Цель", placeholder="К концу урока ученики смогут...")
duration = gr.Slider(label="Длительность (мин)", minimum=30, maximum=180, value=60, step=5)
inventory = gr.Textbox(label="Инвентарь", placeholder="по умолчанию - доска и проектор")
extra_info = gr.Textbox(label="Допонительная информация", placeholder="Класс весёлый и взвинченный после физкультуры")
# Блок 4: Методика
with gr.Column(variant="panel"):
gr.Markdown("### 🎓 Методика", elem_classes=["block-title"])
methodology = gr.Dropdown(label="Методика преподавания",
choices=["PPP (Presentation-Practice-Production)",
"TTT (Test-Teach-Test)"],
value="PPP (Presentation-Practice-Production)")
target_language = gr.Textbox(
label="Target language",
placeholder="Например: 'I have been to...' 'How long have you...?' (Present Perfect experience)",
visible=True # По умолчанию видно, так как PPP выбран
)
# advanced_btn = gr.Button(value="➕ Продвинутые настройки (в разработке", size="sm")
#
# # Блок продвинутых настроек, изначально скрыт
# with gr.Column(visible=False) as advanced_block:
# gr.Markdown("**💡 Уровни задач (таксономия):**")
# repetition = gr.Checkbox(label="Повторение")
# application = gr.Checkbox(label="Применение")
# analysis = gr.Checkbox(label="Анализ")
# creativity = gr.Checkbox(label="Творчество")
hw_required = gr.Checkbox(label="Домашнее задание")
web_search = gr.Checkbox(label="Доп. материалы из интернета")
btn = gr.Button("Создать план", variant="primary", size="lg")
# Правая колонка — результат (output)
with gr.Column(elem_classes=["right-col"], scale=2):
# Общий блок-панель для правой колонки
with gr.Column(variant="panel"): # <<< Главная панель
gr.Markdown("### План урока", elem_classes=["block-title"]) # Заголовок ВНУТРИ панели
# Блок с выводом плана
output = gr.Markdown(
hint_text,
elem_id="plan-output"
)
# Кнопка скачивания (оставляем внутри панели)
download_btn = gr.DownloadButton(
label="⬇️ Скачать .docx",
visible=False
)
# Кнопка "Помогите нам стать лучше"
feedback_btn = gr.Button("💬 Помогите нам стать лучше", elem_classes=["feedback-button"])
# Скрытый блок с обратной связью
with gr.Column(visible=False) as feedback_block:
gr.Markdown("_Спасибо, что попробовали! Как вам?_\n_Ваши наблюдения и замечания помогают нам расти._")
def toggle_feedback_block(current_visible):
return (
gr.update(visible=not current_visible), # показать/скрыть блок
not current_visible, # обновить состояние
gr.update(visible=False) # скрыть благодарность при открытии/закрытии
)
feedback_text = gr.Textbox(
label="Ваше наблюдение или комментарий",
placeholder="Например: 'Для 3 лет лексика подбирается неправильно — какая там 'машина', максимум - 'би-би'",
lines=4
)
rating = gr.Radio(
choices=["1", "2", "3", "4", "5"],
label="Оценка"
)
send_feedback = gr.Button("📩 Отправить отзыв")
feedback_confirmation = gr.Markdown(
visible=False,
elem_classes=["feedback-confirmation"]
)
# <-- Блок благодарности — ВНЕ feedback_block, но СРАЗУ ПОСЛЕ
with gr.Column(visible=False) as feedback_confirmation:
gr.Markdown("✅ Спасибо! Ваш комментарий передан и, возможно, уже сегодня ассистент станет полезнее :)")
# Остальное
gr.Markdown(
"""
🙌 Если вас заинтересовал проект, мы приглашаем
👉 [присоединиться к Telegram-группе](https://t.me/+yN9BSJ1DJxxjZmJi)
"""
)
### --- Логика интерфейса ---
## Доп. настройки
# def toggle_advanced_settings(visible):
# return gr.update(visible=not visible), not visible
# advanced_btn.click(
# fn=toggle_advanced_settings,
# inputs=[advanced_settings_visible],
# outputs=[advanced_block, advanced_settings_visible]
# )
# Формат занятия - видимость поля если выбрано групповое
def toggle_format(selected_format):
return gr.update(visible=selected_format == "Групповое")
format_type.change(fn=toggle_format, inputs=format_type, outputs=group_settings)
# Возраст - деактивация поля если нажато "Взрослые"
def toggle_age(adult_checked):
return gr.update(interactive=not adult_checked)
adults.change(fn=toggle_age, inputs=adults, outputs=age)
# Функция для переключения видимости Target language
def toggle_target_language(methodology_value):
return gr.update(visible=methodology_value == "PPP (Presentation-Practice-Production)")
methodology.change(fn=toggle_target_language, inputs=methodology, outputs=target_language)
# JS для автодополнения
app.load(
None,
None,
js=f"""
const textbooks = {TEXTBOOKS};
function setupAutocomplete() {{
const input = document.getElementById('textbook_input');
if (!input) return;
const datalist = document.createElement('datalist');
datalist.id = 'textbook_suggestions';
textbooks.forEach(book => {{
const option = document.createElement('option');
option.value = book;
datalist.appendChild(option);
}});
input.setAttribute('list', datalist.id);
document.body.appendChild(datalist);
}}
// Запускаем после загрузки
if (document.readyState === 'complete') {{
setupAutocomplete();
}} else {{
document.addEventListener('DOMContentLoaded', setupAutocomplete);
}}
"""
)
### СПИСОК ВСЕХ ПАРАМЕТРОВ ИНТЕРФЕЙСА
all_inputs = [
image, # Gradio компонент, соответствует image_path в функциях
textbook,
cefr,
topic,
goal,
format_type,
num_students,
age,
adults,
level_match,
duration,
inventory,
extra_info,
methodology,
target_language,
hw_required,
web_search
# repetition,
# application,
# analysis,
# creativity
]
# Коллбек генерации
def on_generate(
image_path: Optional[str], # Переименовано из image
textbook: str,
cefr: str,
topic: str,
goal: str,
format_type: str,
num_students: int,
age: str,
adults: bool,
level_match: int,
duration: int,
inventory: str,
extra_info: str,
methodology: str,
target_language: str,
hw_required: bool,
web_search: bool
# repetition: bool,
# application: bool,
# analysis: bool,
# creativity: bool
):
# Проверка обязательных полей
if not image_path or (not adults and not age):
return gr.update(value="❗ Заполните обязательные поля (отмечены *)"), gr.update(visible=False)
# Собираем все аргументы в словарь
kwargs = locals()
# Генерация плана
text = generate_lesson_plan(**kwargs)
# Создание DOCX
docx_path = generate_docx(text) if not text.startswith("❗") else None
return gr.update(value=text), gr.update(visible=bool(docx_path), value=docx_path)
btn.click(
fn=on_generate,
inputs=all_inputs,
outputs=[output, download_btn]
)
# Логика: показать форму по нажатию на кнопку
feedback_btn.click(
fn=toggle_feedback_block,
inputs=[feedback_visible],
outputs=[feedback_block, feedback_visible, feedback_confirmation]
)
# Логика отправки обратной связи
def send_feedback_fn(comment, rate):
save_feedback(comment, rate) # Сохраняем отзыв
return (
gr.update(visible=False), # свернуть форму
False, # сбросить состояние
gr.update(
value="✅ Спасибо! Ваш комментарий передан, и, возможно, уже сегодня ассистент станет полезнее :)",
visible=True
)
)
send_feedback.click(
fn=send_feedback_fn,
inputs=[feedback_text, rating],
outputs=[feedback_block, feedback_visible, feedback_confirmation]
)
if __name__ == "__main__":
app.launch()