PanarinI commited on
Commit
900fbed
·
1 Parent(s): cae90d5
Files changed (2) hide show
  1. app/test_gradio.py +120 -80
  2. docs/architecture.md +41 -0
app/test_gradio.py CHANGED
@@ -1,24 +1,22 @@
1
  import os
 
 
 
2
  from dotenv import load_dotenv
3
  from openai import OpenAI
4
  import gradio as gr
 
5
  import requests
6
- import logging
7
  from typing import Optional
8
 
9
- # Настройка логирования
10
- logging.basicConfig(
11
- level=logging.INFO,
12
- format='%(asctime)s - %(levelname)s - %(message)s'
13
- )
14
-
15
- # Загрузка переменных окружения
16
  load_dotenv()
17
  api_key = os.getenv("API_KEY_openai")
18
  client = OpenAI(api_key=api_key) if api_key else None
19
- VS_ID = os.getenv("VECTOR_STORE_ID", "")
20
-
21
 
 
22
  def upload_to_catbox(file_bytes: bytes) -> str:
23
  """Загружает файл на catbox.moe и возвращает URL"""
24
  try:
@@ -31,100 +29,142 @@ def upload_to_catbox(file_bytes: bytes) -> str:
31
  response.raise_for_status()
32
  return response.text.strip()
33
  except Exception as e:
34
- logging.error(f"Ошибка загрузки: {e}")
35
  raise gr.Error("Не удалось загрузить изображение")
36
 
37
 
 
 
 
 
 
 
 
 
 
 
 
38
  def generate_lesson_plan(
 
39
  textbook: str,
40
- subject: str,
41
- methodology: str,
42
- duration: int,
 
 
 
 
 
43
  hw_required: bool,
44
- file_search: bool,
45
- image_path: Optional[str] = None
46
  ) -> str:
47
- """Генерация детального плана урока"""
48
  if not client:
49
- raise gr.Error("API ключ не настроен или некорректен")
50
-
51
- image_url = None
52
- if image_path:
53
- try:
54
- with open(image_path, "rb") as f:
55
- image_url = upload_to_catbox(f.read())
56
- except Exception as e:
57
- logging.error(f"Image error: {e}")
58
-
59
- # Параметры
60
- content = [
61
- {"type": "message",
62
- "content": (
63
- f"Создай план урока:\n"
64
- f"- Учебник: {textbook}\n"
65
- f"- Предмет: {subject}\n"
66
- )
67
- }
 
 
 
 
68
  ]
69
- if image_url:
70
- content.append({
71
- "type": "image_url",
72
- "image_url": {"url": image_url}
73
- })
74
-
75
- # Инструмент file_search (при наличии)
76
- tools = None
 
 
 
 
 
77
  tool_choice = None
78
- include = None
79
- if file_search and VS_ID:
80
- tools = [{
81
- "type": "file_search",
82
- "vector_store_ids": [VS_ID],
83
- "max_num_results": 10
84
- }]
85
- tool_choice = {"type": "file_search"}
86
- include = ["file_search_call.results"]
87
 
 
88
  try:
89
  response = client.responses.create(
90
- instructions="Ты эксперт-педагог. Составь детальный план урока.",
91
- input=param_vision,
92
  model="gpt-4o-mini",
93
- tools=tools,
94
  tool_choice=tool_choice,
95
- include=include,
96
- max_output_tokens=2000
 
 
97
  )
98
  return response.output_text
 
99
  except Exception as e:
100
  logging.error(f"Generation error: {e}")
101
  raise gr.Error(f"Ошибка генерации: {e}")
102
 
103
-
104
- # Интерфейс Gradio
105
- with gr.Blocks(title="Генератор уроков") as app:
106
  with gr.Row():
107
- with gr.Column():
108
- image = gr.Image(label="Фото страницы учебника (опционально)", type="filepath")
109
- textbook = gr.Textbox(label="Учебник*", placeholder="Название учебника")
110
- subject = gr.Textbox(label="Предмет*", placeholder="Название предмета")
111
- methodology = gr.Dropdown(
112
- label="Методика*",
113
- choices=["PPP", "TTT", "Интерактивная"],
114
- value="PPP"
115
- )
116
- with gr.Accordion("Дополнительно"):
117
- duration = gr.Slider(15, 120, 45, step=5, label="Длительность (мин)")
118
- hw_required = gr.Checkbox(label="Домашнее задание")
119
- file_search = gr.Checkbox(label="Поиск в базах документов")
120
- btn = gr.Button("Сгенерировать", variant="primary")
121
- with gr.Column():
122
- output = gr.Markdown("## План урока")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  btn.click(
125
- fn=generate_lesson_plan,
126
- inputs=[image, textbook, subject, methodology, duration, hw_required, file_search],
127
- outputs=output
128
  )
129
 
130
  if __name__ == "__main__":
 
1
  import os
2
+ import logging
3
+ import tempfile
4
+ from datetime import datetime
5
  from dotenv import load_dotenv
6
  from openai import OpenAI
7
  import gradio as gr
8
+ from docx import Document
9
  import requests
 
10
  from typing import Optional
11
 
12
+ # --- Настройка ---
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
 
 
14
  load_dotenv()
15
  api_key = os.getenv("API_KEY_openai")
16
  client = OpenAI(api_key=api_key) if api_key else None
17
+ VS_ID = os.getenv("VECTOR_STORE_ID", "") # vector store for file_search if needed
 
18
 
19
+ # --- Утилиты ---
20
  def upload_to_catbox(file_bytes: bytes) -> str:
21
  """Загружает файл на catbox.moe и возвращает URL"""
22
  try:
 
29
  response.raise_for_status()
30
  return response.text.strip()
31
  except Exception as e:
32
+ logging.error(f"Ошибка загрузки изображения: {e}")
33
  raise gr.Error("Не удалось загрузить изображение")
34
 
35
 
36
+ def generate_docx(text: str) -> str:
37
+ """Сохраняет текст в .docx и возвращает путь"""
38
+ doc = Document()
39
+ for line in text.split("\n"):
40
+ doc.add_paragraph(line)
41
+ tmp = tempfile.gettempdir()
42
+ path = os.path.join(tmp, f"lesson_plan_{datetime.now().strftime('%Y%m%d%H%M%S')}.docx")
43
+ doc.save(path)
44
+ return path
45
+
46
+ # --- Основная функция генерации ---
47
  def generate_lesson_plan(
48
+ image_path: Optional[str],
49
  textbook: str,
50
+ cefr: str,
51
+ topic: str,
52
+ goal: str,
53
+ format_type: str,
54
+ num_students: int,
55
+ age: str,
56
+ adults: bool,
57
+ level_match: int,
58
  hw_required: bool,
59
+ web_search: bool
 
60
  ) -> str:
61
+ # Валидация API клиента
62
  if not client:
63
+ raise gr.Error("API ключ не настроен")
64
+ # Проверка наличия изображения
65
+ if not image_path:
66
+ return "❗ Загрузите фото страницы учебника для генерации урока"
67
+
68
+ # Загружаем изображение и получаем URL
69
+ try:
70
+ with open(image_path, "rb") as f:
71
+ image_url = upload_to_catbox(f.read())
72
+ except Exception as e:
73
+ logging.error(f"Image upload error: {e}")
74
+ raise gr.Error("Не удалось загрузить изображение")
75
+
76
+ # Собираем текстовый prompt
77
+ params_list = [
78
+ f"- **Учебник**: {textbook}",
79
+ f"- **CEFR-уровень**: {cefr or 'не задан'}",
80
+ f"- **Тема**: {topic or 'не указана'}",
81
+ f"- **Цель занятия**: {goal or 'не указана'}",
82
+ f"- **Формат**: {format_type} ({num_students} {'ребёнок' if num_students==1 else 'детей'})",
83
+ f"- **Возраст**: {'взрослые' if adults else age}",
84
+ f"- **Соответствие уровня учебника**: {['below','on-level','above','mixed'][level_match]}",
85
+ f"- **Домашнее задание**: {'да' if hw_required else 'нет'}"
86
  ]
87
+ if web_search:
88
+ params_list.append("- **Используется web search**: да")
89
+
90
+ prompt_text = "Параметры занятия:\n" + "\n".join(params_list)
91
+
92
+ # Формируем input массив для vision + text
93
+ input_array = [
94
+ {"type": "input_text", "text": prompt_text},
95
+ {"type": "input_image", "image_url": image_url}
96
+ ]
97
+
98
+ # Опции инструментов
99
+ tools = []
100
  tool_choice = None
101
+ # web_search заглушка
102
+ if web_search:
103
+ tools.append({"type": "web_search_preview", "search_context_size": "medium", "user_location": {"type": "approximate", "country": "RU"}})
104
+ tool_choice = {"type": "web_search_preview"}
 
 
 
 
 
105
 
106
+ # Вызов LLM
107
  try:
108
  response = client.responses.create(
109
+ instructions="Ты эксперт-педагог. Составь детальный план урока по заданным параметрам и изображению страницы учебника.",
110
+ input=input_array,
111
  model="gpt-4o-mini",
112
+ tools=tools or None,
113
  tool_choice=tool_choice,
114
+ include=["file_search_call.results"],
115
+ max_output_tokens=2000,
116
+ reasoning={"effort": "medium"},
117
+ stream=False
118
  )
119
  return response.output_text
120
+
121
  except Exception as e:
122
  logging.error(f"Generation error: {e}")
123
  raise gr.Error(f"Ошибка генерации: {e}")
124
 
125
+ # --- Gradio UI ---
126
+ with gr.Blocks(title="AI-Генератор уроков по фото учебника") as app:
 
127
  with gr.Row():
128
+ with gr.Column(scale=1): # левый блок
129
+ image = gr.Image(label="Фото страницы учебника*", type="filepath")
130
+ textbook = gr.Textbox(label="Учебник*", placeholder="Название учебника, напр. English File Beginner")
131
+ cefr = gr.Dropdown(label="CEFR-уровень", choices=["A1","A2","B1","B2","C1","C2"], value="", info="необязательно")
132
+ topic = gr.Textbox(label="Тема", placeholder="напр. Daily routines", info="необязательно")
133
+ goal = gr.Textbox(label="Цель занятия", placeholder="напр. практика Present Simple в вопросах", info="необязательно")
134
+ format_type = gr.Radio(label="Формат занятия", choices=["Индивидуальное","Групповое"], value="Групповое")
135
+ num_students = gr.Slider(label="Количество детей*", minimum=2, maximum=40, value=4, step=1)
136
+ adults = gr.Checkbox(label="Взрослые", info="деактивирует поле возраст")
137
+ age = gr.Textbox(label="Возраст*", placeholder="напр. 10–11")
138
+ def toggle_age(adult_checked):
139
+ return gr.update(interactive=not adult_checked)
140
+ adults.change(fn=toggle_age, inputs=[adults], outputs=[age])
141
+
142
+ level_match = gr.Slider(label="Соответствие уровня учебника", minimum=0, maximum=3, step=1, value=1,
143
+ info="0=below,1=on-level,2=above,3=mixed")
144
+
145
+ hw_required = gr.Checkbox(label="Домашнее задание")
146
+ web_search = gr.Checkbox(label="Доп. материалы из интернета")
147
+ btn = gr.Button("Сгенерировать план", variant="primary")
148
+
149
+ with gr.Column(scale=2): # правый блок
150
+ output = gr.Markdown("## План урока появится здесь...")
151
+ download_btn = gr.DownloadButton(label="⬇️ Скачать .docx", visible=False)
152
+
153
+ # Коллбек генерации
154
+ def on_generate(image, textbook, cefr, topic, goal, format_type, num_students, age, adults, level_match, hw_required, web_search):
155
+ # проверка
156
+ if not image or not textbook or (not age and not adults):
157
+ return gr.update(value="❗ Заполните обязательные поля: фото, учебник, возраст или 'взрослые'"), gr.update(visible=False)
158
+ text = generate_lesson_plan(image, textbook, cefr, topic, goal, format_type, num_students, age, adults, level_match, hw_required, web_search)
159
+ docx_path = None
160
+ if not text.startswith("❗"):
161
+ docx_path = generate_docx(text)
162
+ return gr.update(value=text), gr.update(visible=bool(docx_path), value=docx_path)
163
 
164
  btn.click(
165
+ fn=on_generate,
166
+ inputs=[image, textbook, cefr, topic, goal, format_type, num_students, age, adults, level_match, hw_required, web_search],
167
+ outputs=[output, download_btn]
168
  )
169
 
170
  if __name__ == "__main__":
docs/architecture.md CHANGED
@@ -1,3 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  мне нужно сделать простое приложение по генерации планов уроков на базе предоставленной фотографии страницы учебника.
2
  Для интерфейса используем streamlit. Логика такая - экран делится не левую и правую части. В левой части область загрузки
3
  фотографии страницы учебника + поля заполнения и настройки (они появляются после успешной загрузки).
 
1
+ Я разрабатываю AI-ассистента для учителей английского. Туда загружаешь фото страниц учебника, а также задаешь параметры.
2
+ На выходе получаешь план урока. Фото считывается через vision. Везде используется API openai, эндпоинт responses (2025 год,
3
+ это самый актуальный эндпоинт). Интерфейс реализуется на Gradio. Vision работает через загрузку в него временного url
4
+ изображения страницы учебника. Блоки параметров: 1) Занятия - тема, основная цель, название учебника, уровень учебника
5
+ (по CEFR, выпадающий список) 2) Методика (выпадающий список, там PPP, TTT и другие актуальные подходы и методики ESL - 5 разных).
6
+ Занятие: кол-во учеников (ползунок от 2 до 40 и чекбокс "индивидуальное занятие", нажатие которого деактивирует ползунок),
7
+ время занятия (ползунок от 20 до 120 минут), возраст (свободное поле с placeholder "напр. 10-11", и чекбокс "врослые",
8
+ нажатие которого деактивирует свободное поле с возрастом), соответствие класса уровню - ползунок с 3 позициями mixed ability
9
+ (предложи варианты, ведь бывает что учебник ниже уровня большинства, бывает наоборот ниже, бывает что в примерно в уровень,
10
+ бывает полный mixed ability). Дальше - ччекбокс наличия домашнего задания, под ним - чекбокс доп. материалов из интернета.
11
+ Обязательные поля должны быть заполнены для активации генерции, иначе при нажатии кнопки создания урока на месте плана урока
12
+ возникает сообщение о необходимости заполнить обязательные поля. Оформлен интерфейс в 2 колонки - левая с настройками (1/3 ширины),
13
+ правая с планом урока (2/3 ширины). Я тебе вышлю имеющуюся у меня заготовку - здесь корректна инициализация клиента, правильные
14
+ названия эндпоинтов и моделей. Но здесь практически всё пока не то, что нужно - image_url сейчас подключается по условию, но оно
15
+ должно всегда попадать в LLM для генерации и другое. Прежде чем начнем писать код, проговори всё, и задай если нужно вопросы. Код не пиши.
16
+
17
+ На вопросы: 1. методики - давай пока оставим эти 2 - PPP и TTT. По умолчанию - PPP, так что поле необязательное (значение по умолчанию уже есть) 2.метки для уровня:
18
+ “below”, “on-level”, “above”, “mixed” 3. Да, правильно 4. Если фото не загружено - урок не генерируется (это ключевая функция ассистента - создавать урок по загруженной странице)
19
+ 5. Формат вывода - markdown внутри правого поля внутри Gradio - ок, но docx-экспорт через кнопку должен тоже быть (так же, как в приложении из примера) 6.
20
+ 6. Заглушка web search - она не возвращает ничего, в prompt можно ее добавить при условии нажатия чекбокса как "используется web search
21
+ 7. (пусть модель просто видит эту строчку если чекбокс нажат, а если отжат - там ничего) 7. Достаточно слайдера, о котором говорили выше.
22
+ 8. Что еще забыли -- параметры тема и цель занятия. Тема необязательная, цель - тоже необязательная. Все настройки делим на блоки (аналогично тому
23
+ 9. как в приложении примере есть блоки "ребенок" и "занятие"). В начале - блок "занятие" (учебник, уровень, тема, цель"), затем - ��ласс (кол-во детей
24
+ 10. (обязательное поле), возраст (обязательно), соответствие уровню учебника. Потом чекбоксы - наличие ДЗ и доп. материалы из интернета. Во всех полях
25
+ 11. со свободным ответом должны быть плейсхолдеры с релевантным текстом-подсказкой
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
  мне нужно сделать простое приложение по генерации планов уроков на базе предоставленной фотографии страницы учебника.
43
  Для интерфейса используем streamlit. Логика такая - экран делится не левую и правую части. В левой части область загрузки
44
  фотографии страницы учебника + поля заполнения и настройки (они появляются после успешной загрузки).