duksa commited on
Commit
8976b6d
·
verified ·
1 Parent(s): 251fa05

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +159 -8
app.py CHANGED
@@ -20,6 +20,15 @@ try:
20
  except ImportError:
21
  GSPREAD_AVAILABLE = False
22
 
 
 
 
 
 
 
 
 
 
23
  try:
24
  import spaces # доступно только внутри HuggingFace Spaces
25
  except ImportError:
@@ -48,10 +57,15 @@ SPREADSHEET_ID = os.environ.get(
48
  GOOGLE_SERVICE_ACCOUNT_ENV = "GOOGLE_SERVICE_ACCOUNT_JSON"
49
 
50
  SHEETS_SCOPES = [
51
- "https://www.googleapis.com/auth/spreadsheets.readonly",
52
  "https://www.googleapis.com/auth/drive.readonly",
53
  ]
54
 
 
 
 
 
 
55
  # Максимальная длина раздела учебника, которая уходит в промпт.
56
  # Ограничивает расход токенов Groq API даже для очень длинных глав.
57
  MAX_TEXTBOOK_SECTION_CHARS = 6000
@@ -327,6 +341,113 @@ def load_class_data(class_name: str):
327
  return topics, textbook_sections, status
328
 
329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  # ---------------------------------------------------------------------------
331
  # Логика генерации
332
  # ---------------------------------------------------------------------------
@@ -390,7 +511,7 @@ def render_level_html(level_key: str, tasks: list) -> str:
390
 
391
 
392
  @spaces.GPU
393
- def generate_tasks(dropdown_value, custom_topic, n_basic, n_medium, n_advanced, textbook_sections_state):
394
  topic = build_topic(dropdown_value, custom_topic)
395
 
396
  if not topic:
@@ -453,6 +574,13 @@ def generate_tasks(dropdown_value, custom_topic, n_basic, n_medium, n_advanced,
453
  if source_note:
454
  header += f"<p style='color:#2e7d32; font-size:0.9em;'>{source_note}</p>"
455
 
 
 
 
 
 
 
 
456
  html_parts = [header]
457
  for level_key in ["базовый", "средний", "продвинутый"]:
458
  html_parts.append(render_level_html(level_key, by_level[level_key]))
@@ -513,10 +641,16 @@ def on_class_selected(class_name):
513
 
514
  if not topics:
515
  choices = DEFAULT_TOPICS + [CUSTOM_TOPIC_LABEL]
516
- return gr.update(choices=choices, value=choices[0]), {}, status
517
 
518
  choices = topics + [CUSTOM_TOPIC_LABEL]
519
- return gr.update(choices=choices, value=choices[0]), textbook_sections, status
 
 
 
 
 
 
520
 
521
 
522
  # ---------------------------------------------------------------------------
@@ -530,13 +664,15 @@ with gr.Blocks(title="Генератор разноуровневых задан
530
  Выберите тему (или введите свою), укажите количество заданий на каждый уровень сложности — и получите готовый комплект с ответами и методическими комментариями.
531
  """)
532
 
533
- # Скрытое состояние — распарсенные разделы учебника {тема: текст}
534
  textbook_sections_state = gr.State({})
 
535
 
536
  with gr.Accordion("📊 Источник: Google Таблица с темами по классам", open=True):
537
  gr.Markdown(
538
  "Таблица содержит листы по классам. В каждом листе: 1-я строка — ссылка "
539
- "на .md учебника на Google Drive, остальные строки — темы."
 
540
  )
541
  with gr.Row():
542
  refresh_classes_btn = gr.Button("🔄 Загрузить список классов")
@@ -580,6 +716,15 @@ with gr.Blocks(title="Генератор разноуровневых задан
580
  with gr.Column(scale=2):
581
  output_html = gr.HTML(label="Результат")
582
 
 
 
 
 
 
 
 
 
 
583
  refresh_classes_btn.click(
584
  fn=on_refresh_classes,
585
  inputs=[],
@@ -589,7 +734,7 @@ with gr.Blocks(title="Генератор разноуровневых задан
589
  class_dropdown.change(
590
  fn=on_class_selected,
591
  inputs=[class_dropdown],
592
- outputs=[topic_dropdown, textbook_sections_state, sheets_status],
593
  )
594
 
595
  topics_file.change(
@@ -606,9 +751,15 @@ with gr.Blocks(title="Генератор разноуровневых задан
606
 
607
  generate_btn.click(
608
  fn=generate_tasks,
609
- inputs=[topic_dropdown, custom_topic_input, n_basic, n_medium, n_advanced, textbook_sections_state],
610
  outputs=output_html,
611
  )
612
 
 
 
 
 
 
 
613
  if __name__ == "__main__":
614
  demo.launch()
 
20
  except ImportError:
21
  GSPREAD_AVAILABLE = False
22
 
23
+ try:
24
+ import pandas as pd
25
+ import matplotlib
26
+ matplotlib.use("Agg")
27
+ import matplotlib.pyplot as plt
28
+ PANDAS_AVAILABLE = True
29
+ except ImportError:
30
+ PANDAS_AVAILABLE = False
31
+
32
  try:
33
  import spaces # доступно только внутри HuggingFace Spaces
34
  except ImportError:
 
57
  GOOGLE_SERVICE_ACCOUNT_ENV = "GOOGLE_SERVICE_ACCOUNT_JSON"
58
 
59
  SHEETS_SCOPES = [
60
+ "https://www.googleapis.com/auth/spreadsheets", # чтение и запись
61
  "https://www.googleapis.com/auth/drive.readonly",
62
  ]
63
 
64
+ # Раскладка столбцов на листе класса: A — тема (уже используется),
65
+ # B/C/D — сгенерированные задания по уровням, E — счётчик генераций по теме
66
+ RESULT_COLUMNS = {"базовый": 2, "средний": 3, "продвинутый": 4} # B, C, D
67
+ GENERATION_COUNT_COLUMN = 5 # E
68
+
69
  # Максимальная длина раздела учебника, которая уходит в промпт.
70
  # Ограничивает расход токенов Groq API даже для очень длинных глав.
71
  MAX_TEXTBOOK_SECTION_CHARS = 6000
 
341
  return topics, textbook_sections, status
342
 
343
 
344
+ def format_tasks_for_cell(tasks: list) -> str:
345
+ """Форматирует список заданий одного уровня в текст для одной ячейки таблицы."""
346
+ if not tasks:
347
+ return ""
348
+
349
+ blocks = []
350
+ for i, t in enumerate(tasks, start=1):
351
+ blocks.append(
352
+ f"Задание {i}: {t.get('задание', '—')}\n"
353
+ f"Ответ: {t.get('ответ', '—')}\n"
354
+ f"Комментарий: {t.get('комментарий', '—')}"
355
+ )
356
+ return "\n\n".join(blocks)
357
+
358
+
359
+ def write_result_to_sheet(class_name: str, topic: str, by_level: dict) -> str:
360
+ """
361
+ Записывает (с перезаписью) сгенерированные задания в столбцы B/C/D строки темы
362
+ и увеличивает счётчик генераций в столбце E. Работает только если тема реально
363
+ взята из листа таблицы (найдена точным совпадением в столбце A) — для тем,
364
+ введённых вручную, запись молча пропускается.
365
+ """
366
+ if not class_name or not topic:
367
+ return ""
368
+
369
+ try:
370
+ gc = get_sheets_client()
371
+ sh = gc.open_by_key(SPREADSHEET_ID)
372
+ ws = sh.worksheet(class_name)
373
+ col_values = ws.col_values(1)
374
+ except Exception as e:
375
+ return f"⚠️ Не удалось записать результат в таблицу: {e}"
376
+
377
+ row_index = None
378
+ for i, value in enumerate(col_values, start=1):
379
+ if value.strip() == topic.strip():
380
+ row_index = i
381
+ break
382
+
383
+ if row_index is None:
384
+ # Тема введена вручную и её нет в листе — запись в таблицу не имеет смысла
385
+ return ""
386
+
387
+ try:
388
+ for level, col in RESULT_COLUMNS.items():
389
+ cell_text = format_tasks_for_cell(by_level.get(level, []))
390
+ ws.update_cell(row_index, col, cell_text)
391
+
392
+ current_count_values = ws.col_values(GENERATION_COUNT_COLUMN)
393
+ current_count = 0
394
+ if len(current_count_values) >= row_index:
395
+ try:
396
+ current_count = int(current_count_values[row_index - 1])
397
+ except (ValueError, IndexError):
398
+ current_count = 0
399
+ ws.update_cell(row_index, GENERATION_COUNT_COLUMN, current_count + 1)
400
+ except Exception as e:
401
+ return f"⚠️ Задания сгенерированы, но не удалось записать их в таблицу: {e}"
402
+
403
+ return f"💾 Результат записан в таблицу (строка {row_index}, лист «{class_name}»)."
404
+
405
+
406
+ def build_usage_stats(class_name: str):
407
+ """
408
+ Читает столбцы A (темы) и E (счётчик генераций) выбранного листа через Pandas
409
+ и строит график частоты запросов по темам (Matplotlib).
410
+ Возвращает (figure, status_message); figure может быть None при ошибке/отсутствии данных.
411
+ """
412
+ if not PANDAS_AVAILABLE:
413
+ return None, "⚠️ Pandas/Matplotlib не установлены."
414
+
415
+ if not class_name:
416
+ return None, "Класс не выбран."
417
+
418
+ try:
419
+ gc = get_sheets_client()
420
+ sh = gc.open_by_key(SPREADSHEET_ID)
421
+ ws = sh.worksheet(class_name)
422
+ topics_col = ws.col_values(1)[1:] # без 1-й строки (ссылка на учебник)
423
+ counts_col = ws.col_values(GENERATION_COUNT_COLUMN)[1:]
424
+ except Exception as e:
425
+ return None, f"⚠️ Не удалось прочитать данные для статистики: {e}"
426
+
427
+ # Выравниваем длины столбцов и приводим счётчик к числу через Pandas
428
+ n = len(topics_col)
429
+ counts_col = (counts_col + [""] * n)[:n]
430
+
431
+ df = pd.DataFrame({"тема": topics_col, "генераций": counts_col})
432
+ df["генераций"] = pd.to_numeric(df["генераций"], errors="coerce").fillna(0).astype(int)
433
+ df = df[df["тема"].str.strip() != ""]
434
+
435
+ if df["генераций"].sum() == 0:
436
+ return None, "Пока нет ни одной генерации по темам этого класса — статистика появится после первых запусков."
437
+
438
+ df_sorted = df.sort_values("генераций", ascending=False).head(10)
439
+
440
+ fig, ax = plt.subplots(figsize=(8, 4.5))
441
+ ax.barh(df_sorted["тема"], df_sorted["генераций"], color="#1E8F7A")
442
+ ax.set_xlabel("Количество генераций")
443
+ ax.set_title(f"Частота запросов по темам — {class_name}")
444
+ ax.invert_yaxis()
445
+ fig.tight_layout()
446
+
447
+ total_generations = int(df["генераций"].sum())
448
+ return fig, f"Всего генераций по классу «{class_name}»: {total_generations}"
449
+
450
+
451
  # ---------------------------------------------------------------------------
452
  # Логика генерации
453
  # ---------------------------------------------------------------------------
 
511
 
512
 
513
  @spaces.GPU
514
+ def generate_tasks(dropdown_value, custom_topic, n_basic, n_medium, n_advanced, textbook_sections_state, class_state):
515
  topic = build_topic(dropdown_value, custom_topic)
516
 
517
  if not topic:
 
574
  if source_note:
575
  header += f"<p style='color:#2e7d32; font-size:0.9em;'>{source_note}</p>"
576
 
577
+ # Запись результата обратно в таблицу (если тема пришла из листа выбранного класса)
578
+ if tasks and class_state:
579
+ write_status = write_result_to_sheet(class_state, topic, by_level)
580
+ if write_status:
581
+ color = "#2e7d32" if write_status.startswith("💾") else "#b8860b"
582
+ header += f"<p style='color:{color}; font-size:0.9em;'>{write_status}</p>"
583
+
584
  html_parts = [header]
585
  for level_key in ["базовый", "средний", "продвинутый"]:
586
  html_parts.append(render_level_html(level_key, by_level[level_key]))
 
641
 
642
  if not topics:
643
  choices = DEFAULT_TOPICS + [CUSTOM_TOPIC_LABEL]
644
+ return gr.update(choices=choices, value=choices[0]), {}, status, None
645
 
646
  choices = topics + [CUSTOM_TOPIC_LABEL]
647
+ return gr.update(choices=choices, value=choices[0]), textbook_sections, status, class_name
648
+
649
+
650
+ def on_refresh_stats(class_name):
651
+ """Строит график статистики использования тем через Pandas/Matplotlib."""
652
+ fig, status = build_usage_stats(class_name)
653
+ return fig, status
654
 
655
 
656
  # ---------------------------------------------------------------------------
 
664
  Выберите тему (или введите свою), укажите количество заданий на каждый уровень сложности — и получите готовый комплект с ответами и методическими комментариями.
665
  """)
666
 
667
+ # Скрытое состояние — распарсенные разделы учебника {тема: текст} и текущий класс
668
  textbook_sections_state = gr.State({})
669
+ class_state = gr.State(None)
670
 
671
  with gr.Accordion("📊 Источник: Google Таблица с темами по классам", open=True):
672
  gr.Markdown(
673
  "Таблица содержит листы по классам. В каждом листе: 1-я строка — ссылка "
674
+ "на .md учебника на Google Drive, остальные строки — темы. Столбцы B–D "
675
+ "��аполняются сгенерированными заданиями по уровням, столбец E — счётчик генераций."
676
  )
677
  with gr.Row():
678
  refresh_classes_btn = gr.Button("🔄 Загрузить список классов")
 
716
  with gr.Column(scale=2):
717
  output_html = gr.HTML(label="Результат")
718
 
719
+ with gr.Accordion("📈 Статистика использования по темам (Pandas + Matplotlib)", open=False):
720
+ gr.Markdown(
721
+ "Читает столбец с количеством генераций по каждой теме выбранного класса "
722
+ "и строит график самых часто запрашиваемых тем."
723
+ )
724
+ refresh_stats_btn = gr.Button("Обновить статистику")
725
+ stats_status = gr.Markdown("Выберите класс выше и нажмите «Обновить статистику».")
726
+ stats_plot = gr.Plot(label="Частота запросов по темам")
727
+
728
  refresh_classes_btn.click(
729
  fn=on_refresh_classes,
730
  inputs=[],
 
734
  class_dropdown.change(
735
  fn=on_class_selected,
736
  inputs=[class_dropdown],
737
+ outputs=[topic_dropdown, textbook_sections_state, sheets_status, class_state],
738
  )
739
 
740
  topics_file.change(
 
751
 
752
  generate_btn.click(
753
  fn=generate_tasks,
754
+ inputs=[topic_dropdown, custom_topic_input, n_basic, n_medium, n_advanced, textbook_sections_state, class_state],
755
  outputs=output_html,
756
  )
757
 
758
+ refresh_stats_btn.click(
759
+ fn=on_refresh_stats,
760
+ inputs=[class_state],
761
+ outputs=[stats_plot, stats_status],
762
+ )
763
+
764
  if __name__ == "__main__":
765
  demo.launch()