Spaces:
Sleeping
Sleeping
| import os | |
| from dotenv import load_dotenv | |
| from openai import OpenAI | |
| import gradio as gr | |
| import time | |
| from datetime import datetime | |
| import calendar | |
| from docx import Document | |
| import tempfile | |
| import random # для случайного выбора цитаты | |
| from app.quotes import quotes | |
| import logging | |
| # Настройка логирования | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(levelname)s - %(message)s', | |
| handlers=[logging.StreamHandler()] | |
| ) # Вывод в консоль | |
| # Загрузка переменных окружения | |
| load_dotenv() | |
| api_key = os.getenv("API_KEY_openai") | |
| client = OpenAI(api_key=api_key) | |
| # API_KEY = os.getenv("API_KEY") | |
| # BASE_URL = os.getenv("BASE_URL") | |
| # client = OpenAI(api_key=API_KEY, base_url=BASE_URL) | |
| # Функция генерации плана занятия | |
| def generate_lesson_plan_interface( | |
| нарушение, возраст_ребенка, особые_условия, | |
| формат_занятия, количество_детей, цель_занятия, тема, длительность_занятия, | |
| инвентарь, наличие_ДЗ, разрешен_web_search, текущий_месяц=None | |
| ): | |
| # Логика работы с источниками - если есть, передаем | |
| #источники = [web_sources] if разрешен_web_search and web_sources else [] | |
| # Логика определения месяца | |
| if not текущий_месяц: | |
| текущий_месяц = calendar.month_name[datetime.now().month] | |
| # все параметры | |
| params = { | |
| "нарушение": нарушение, | |
| "возраст_ребенка": возраст_ребенка, | |
| "цель_занятия": цель_занятия, | |
| "формат_занятия": формат_занятия, | |
| "количество_детей": количество_детей, | |
| "тема": тема, | |
| "особые_условия": особые_условия, | |
| "инвентарь": инвентарь, | |
| "длительность_занятия": длительность_занятия, | |
| "наличие_ДЗ": наличие_ДЗ, | |
| "разрешен_web_search": разрешен_web_search, | |
| #"источники": источники, | |
| "текущий_месяц": текущий_месяц | |
| } | |
| if формат_занятия == "Индивидуальное": | |
| params["количество_детей"] = 1 | |
| # Формируем основной промпт | |
| base_prompt = f""" | |
| Ты — эксперт-логопед, разрабатывающий занятия для детей с речевыми нарушениями. | |
| На основе параметров занятия составь план занятия, который должен включать: | |
| 1. **Тема занятия, цель и задачи** (не более 3-х) | |
| 2. **Необходимый инвентарь** | |
| 3. **Ход занятия** – логически последовательные этапы с конкретными упражнениями и примерами""" | |
| # Добавляем секцию для веб-поиска только если разрешено | |
| web_search_section = "" | |
| if разрешен_web_search: | |
| web_search_section = """ | |
| **ВНИМАНИЕ:** Используй актуальные данные из интернета: | |
| - Найди примеры релевантных упражнений и интегрируй их в этапы | |
| - При необходимости адаптируй упражнения по параметрам ребенка и занятия | |
| - Для каждого заимствованного упражнения укажи источник (ссылку)""" | |
| # Продолжение базового промпта | |
| continuation = f""" | |
| 4. **Домашнее задание** (только если параметр "наличие домашнего задания" = True) | |
| 5. **Рекомендации по особым условиям** (если указаны) | |
| Параметры занятия: | |
| - **Основное нарушение:** {нарушение} | |
| - **Возраст:** {возраст_ребенка} | |
| - **Цель занятия:** {цель_занятия} | |
| - **Тема:** {тема or "не указано - определи самостоятельно"} | |
| - **Формат:** {формат_занятия} ({количество_детей} детей) | |
| - **Инвентарь:** {инвентарь or "не указан - предложи варианты"} | |
| - **Особые условия:** {особые_условия or "отсутствуют"} | |
| - **Длительность:** {длительность_занятия} минут | |
| - **Месяц года:** {текущий_месяц} | |
| Стиль оформления: | |
| - Четкая структура с нумерованными пунктами | |
| - Минимум общих фраз, максимум конкретики | |
| - Возрастосообразные примеры и материалы""" | |
| # Собираем полный промпт | |
| full_prompt = base_prompt + web_search_section + continuation | |
| tools = [] | |
| tool_choice = None | |
| if params['разрешен_web_search']: | |
| tools.append({ | |
| "type": "web_search_preview", | |
| "search_context_size": "medium", | |
| "user_location": {"type": "approximate", "country": "RU"} | |
| }) | |
| tool_choice = {"type": "web_search_preview"} | |
| response = client.responses.create( | |
| model="gpt-4o-mini", | |
| input=full_prompt, | |
| temperature=0.5, | |
| tools=tools if tools else None, | |
| tool_choice=tool_choice, | |
| max_output_tokens=2000, | |
| stream=True | |
| ) | |
| ####### БЕЗ СТРИМИНГА | |
| # return response.output_text # Основной вывод без изменений | |
| # АННОТАЦИИ В ЛОГ | |
| # if params['разрешен_web_search']: # Только логируем аннотации при веб-поиске | |
| # try: | |
| # content_block = response.output[1].content[0] | |
| # # logging.info(f"=== ПОЛНЫЙ КОНТЕНТ БЛОКА ===") | |
| # # logging.info(f"Тип: {content_block.type}") | |
| # # logging.info(f"Текст: {content_block.text[:200]}...") # Первые 200 символов текста | |
| # logging.info(f"Аннотации: {content_block.annotations}") | |
| # # logging.info(f"Сырые данные: {vars(content_block)}") # Вся техническая информация | |
| # except (IndexError, AttributeError) as e: | |
| # logging.warning(f"Не удалось получить аннотации: {str(e)}") | |
| ####### СТРИМИНГ | |
| try: | |
| for event in response: | |
| if event.type == 'response.output_text.delta': | |
| yield event.delta | |
| elif event.type == 'response.completed': | |
| break | |
| except Exception as e: | |
| yield f"Ошибка: {str(e)}" | |
| ############# COMPLETIONS (РАБОТАЕТ БЕЗ TOOLS) | |
| # response = client.chat.completions.create( | |
| # model="gpt-4o-mini", | |
| # messages=[ | |
| # {"role": "system", "content": "Ты — эксперт в области логопедии."}, | |
| # {"role": "user", "content": prompt} | |
| # ], | |
| # max_tokens=2000, | |
| # stream=True # Включаем потоковый режим | |
| # ) | |
| # for chunk in response: | |
| # if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content: | |
| # yield chunk.choices[0].delta.content | |
| ### css привязывать именно так | |
| css_path = os.path.join(os.path.dirname(__file__), "styles.css") | |
| theme='earneleh/paris' | |
| #, css_paths=css_path | |
| # Интерфейс Gradio | |
| with gr.Blocks(theme=theme) as demo: | |
| 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): | |
| gr.Markdown("### 🧒 Ребёнок", elem_classes=["block-title"]) | |
| нарушение = gr.Textbox(label="Основное нарушение*", placeholder="Пример: Дислалия (свистящие), ОНР II уровня") | |
| возраст = gr.Textbox(label="Возраст ребенка*", placeholder="Пример: 5 лет, 6-7 лет") | |
| особые_условия = gr.Textbox(label="Особые условия", placeholder="Пример: гиперактивность, РАС") | |
| gr.Markdown("### 📄 Занятие", elem_classes=["block-title"]) | |
| формат = gr.Radio(["Индивидуальное", "Групповое"], label="Формат занятия", value="Индивидуальное") | |
| количество_детей = gr.Slider( | |
| label="Количество детей в группе", minimum=2, maximum=10, value=2, step=1, visible=False | |
| ) | |
| def toggle_group_slider(selected_format): | |
| return gr.update(visible=(selected_format == "Групповое")) | |
| формат.change(fn=toggle_group_slider, inputs=формат, outputs=количество_детей) | |
| цель = gr.Textbox(label="Цель занятия*", placeholder="Пример: Автоматизация звука [Р] в слогах") | |
| тема = gr.Textbox(label="Тема", placeholder="Пример: Животные, Транспорт") | |
| длительность = gr.Slider(label="Длительность занятия (мин)", minimum=15, maximum=60, value=30, step=5) | |
| инвентарь = gr.Textbox(label="Инвентарь (через запятую)", | |
| placeholder="Пример: Зеркало, Карточки, Куклы") | |
| дз = gr.Checkbox(label="Домашнее задание") | |
| # Выделенный блок для профессиональных ресурсов | |
| gr.Markdown("---") # Разделительная линия | |
| # gr.Markdown("### 🔍 Дополнительные ресурсы", elem_classes=["block-title", "pro-title"]) | |
| with gr.Row(variant="panel"): # Вариант "panel" добавляет фоновый оттенок | |
| web = gr.Checkbox( | |
| label="📚 Поиск упражнений в логопедических базах", | |
| info="Упражнения подбираются из проверенных источников со ссылками", | |
| interactive=True | |
| ) | |
| #web_sources = gr.Textbox( | |
| # label="Уточнить источники (необязательно, через запятую):", | |
| # placeholder="Например: Logopedy.ru, Maam.ru, ...", | |
| # visible=False, | |
| #) | |
| # Логика отображения поля источников | |
| #web.change( | |
| # fn=lambda x: gr.update(visible=x), | |
| # inputs=web, | |
| # outputs=web_sources | |
| #) | |
| # Кнопка создания конспекта | |
| btn = gr.Button("Создать конспект", variant="primary") | |
| # Правая колонка — результат (output) | |
| with gr.Column(elem_classes=["right-col"], scale=2): # Правая колонка — результат | |
| gr.Markdown("### План урока", elem_classes=["block-title"]) | |
| output = gr.Markdown( | |
| "Здесь появится план урока после генерации.", | |
| elem_id="plan-output" | |
| ) | |
| download_btn = gr.DownloadButton( | |
| label="⬇️ Скачать .docx", | |
| visible=False | |
| ) | |
| output = gr.Markdown("") # Поле для вывода конспекта | |
| # Ввод параметров | |
| all_inputs = [ | |
| нарушение, возраст, особые_условия, | |
| формат, количество_детей, цель, тема, длительность, | |
| инвентарь, дз, web #web_sources | |
| ] | |
| # Функция для генерации docx файла | |
| def generate_docx(text: str): | |
| doc = Document() | |
| for line in text.split("\n"): | |
| doc.add_paragraph(line) | |
| tmp_dir = tempfile.gettempdir() | |
| file_path = os.path.join(tmp_dir, "Конспект_занятия.docx") | |
| doc.save(file_path) | |
| return file_path | |
| ################### СТРИМИНГ | |
| def on_submit_with_spinner(*args): | |
| # Проверка обязательных полей (остается без изменений) | |
| if not args[0] or not args[1] or not args[5]: | |
| yield ( | |
| *[gr.update(interactive=True) for _ in all_inputs], | |
| gr.update(value="❗Заполните обязательные поля: нарушение, возраст, цель занятия"), | |
| gr.update(visible=False) | |
| ) | |
| return | |
| # Блокируем интерфейс | |
| yield ( | |
| *[gr.update(interactive=False) for _ in all_inputs], | |
| gr.update(value="⏳ Конспект создается..."), | |
| gr.update(visible=False) | |
| ) | |
| full_response = [] | |
| try: | |
| for chunk in generate_lesson_plan_interface(*args): | |
| full_response.append(chunk) | |
| yield ( | |
| *[gr.update(interactive=False) for _ in all_inputs], | |
| gr.update(value="".join(full_response)), | |
| gr.update(visible=False) | |
| ) | |
| # После завершения стрима | |
| file_path = generate_docx("".join(full_response)) | |
| yield ( | |
| *[gr.update(interactive=True) for _ in all_inputs], | |
| gr.update(value="".join(full_response)), | |
| gr.update(visible=True, value=file_path) | |
| ) | |
| except Exception as e: | |
| yield ( | |
| *[gr.update(interactive=True) for _ in all_inputs], | |
| gr.update(value=f"❌ Ошибка: {str(e)}"), | |
| gr.update(visible=False) | |
| ) | |
| ################## БЕЗ СТРИМИНГА | |
| # def on_submit_with_spinner(*args): | |
| # if not args[0] or not args[1] or not args[5]: | |
| # return ( | |
| # *[gr.update(interactive=True) for _ in all_inputs], | |
| # gr.update(value="❗Заполните обязательные поля: нарушение, возраст, цель занятия"), | |
| # gr.update(visible=False) | |
| # ) | |
| # Блокируем интерфейс | |
| # try: | |
| # response_text = generate_lesson_plan_interface(*args) | |
| # file_path = generate_docx(response_text) | |
| # return ( | |
| # *[gr.update(interactive=True) for _ in all_inputs], | |
| # gr.update(value=response_text), | |
| # gr.update(visible=True, value=file_path) | |
| # ) | |
| # except Exception as e: | |
| # return ( | |
| # *[gr.update(interactive=True) for _ in all_inputs], | |
| # gr.update(value=f"❌ Ошибка: {str(e)}"), | |
| # gr.update(visible=False) | |
| # ) | |
| # Привязываем обработчики | |
| btn.click( | |
| fn=on_submit_with_spinner, | |
| inputs=all_inputs, | |
| outputs=[*all_inputs, output, download_btn] # <- скобку если не стримминг | |
| ).then( | |
| lambda: None, | |
| inputs=[], | |
| outputs=[] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(share=True) |