Anonumous commited on
Commit
20b4d4f
·
1 Parent(s): b020a59

Update benchmark

Browse files
Files changed (5) hide show
  1. README.md +8 -6
  2. app.py +187 -12
  3. constants.py +12 -75
  4. styles.py +62 -13
  5. utils.py +142 -19
README.md CHANGED
@@ -1,12 +1,14 @@
1
  ---
2
  title: Russian ASR Leaderboard
3
- emoji: 👁
4
- colorFrom: purple
5
- colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 5.41.1
8
  app_file: app.py
9
- pinned: false
 
 
 
 
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Russian ASR Leaderboard
3
+ emoji: 🎶
4
+ colorFrom: azure
5
+ colorTo: teal
6
  sdk: gradio
7
  sdk_version: 5.41.1
8
  app_file: app.py
9
+ pinned: true
10
+ license: apache-2.0
11
+ tags:
12
+ - asr
13
+ - leaderboard
14
  ---
 
 
app.py CHANGED
@@ -1,15 +1,28 @@
1
  import gradio as gr
 
2
 
3
- from constants import INTRODUCTION_TEXT, METRICS_TAB_TEXT, SUBMIT_TAB_TEXT
4
- from utils import init_repo, load_data, process_submit, get_datasets_description
 
 
 
 
 
 
 
 
 
5
  from styles import LEADERBOARD_CSS
6
 
7
  init_repo()
 
8
 
9
  with gr.Blocks(css=LEADERBOARD_CSS, theme=gr.themes.Soft()) as demo:
10
  gr.HTML(
11
- '<img src="https://cdn.donmai.us/original/67/9c/__taihou_and_surcouf_azur_lane_and_1_more_drawn_by_yunsang__679c42b017a91a2349b25acfc7935157.png" style="width: 100%; height: auto;">'
 
12
  )
 
13
  gr.Markdown(INTRODUCTION_TEXT, elem_classes="markdown-text")
14
 
15
  with gr.Tabs():
@@ -17,20 +30,182 @@ with gr.Blocks(css=LEADERBOARD_CSS, theme=gr.themes.Soft()) as demo:
17
  leaderboard_html = gr.HTML(value=load_data(), every=60)
18
 
19
  with gr.Tab("📈 Метрики"):
20
- gr.Markdown(METRICS_TAB_TEXT, elem_classes="markdown-text")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  with gr.Tab("📊 Датасеты"):
23
- gr.Markdown(get_datasets_description(), elem_classes="markdown-text")
24
 
25
  with gr.Tab("✉️ Отправить результат"):
26
- gr.Markdown(SUBMIT_TAB_TEXT, elem_classes="markdown-text")
27
- json_input = gr.TextArea(label="JSON")
28
- submit_btn = gr.Button("🚀 Отправить")
29
- output_msg = gr.Textbox(label="Статус")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  submit_btn.click(
31
- process_submit,
32
- inputs=json_input,
33
- outputs=[leaderboard_html, output_msg, json_input],
34
  )
35
 
36
  demo.launch()
 
1
  import gradio as gr
2
+ import json
3
 
4
+ from constants import INTRODUCTION_TEXT
5
+ from utils import (
6
+ init_repo,
7
+ load_data,
8
+ process_submit,
9
+ get_datasets_description,
10
+ get_metrics_html,
11
+ compute_wer_cer,
12
+ get_submit_html,
13
+ DATASETS,
14
+ )
15
  from styles import LEADERBOARD_CSS
16
 
17
  init_repo()
18
+ gr.set_static_paths(paths=["."])
19
 
20
  with gr.Blocks(css=LEADERBOARD_CSS, theme=gr.themes.Soft()) as demo:
21
  gr.HTML(
22
+ '<img src="/gradio_api/file=Logo.png" '
23
+ 'style="display:block; margin:0 auto; width:34%; height:auto;">'
24
  )
25
+
26
  gr.Markdown(INTRODUCTION_TEXT, elem_classes="markdown-text")
27
 
28
  with gr.Tabs():
 
30
  leaderboard_html = gr.HTML(value=load_data(), every=60)
31
 
32
  with gr.Tab("📈 Метрики"):
33
+ gr.HTML(get_metrics_html())
34
+ with gr.Group():
35
+ gr.Markdown("### Песочница: посчитайте WER/CER на своих строках")
36
+ with gr.Row():
37
+ ref = gr.Textbox(
38
+ label="Референсный текст",
39
+ placeholder="например: я люблю машинное обучение",
40
+ lines=2,
41
+ )
42
+ hyp = gr.Textbox(
43
+ label="Гипотеза (распознанный текст)",
44
+ placeholder="например: я люблю мощинное обучение",
45
+ lines=2,
46
+ )
47
+ with gr.Row():
48
+ normalize = gr.Checkbox(
49
+ value=True,
50
+ label="Нормализовать (нижний регистр, без пунктуации)",
51
+ )
52
+ btn_calc = gr.Button("Посчитать")
53
+ with gr.Row():
54
+ out_wer = gr.Number(label="WER, %", precision=2)
55
+ out_cer = gr.Number(label="CER, %", precision=2)
56
+
57
+ def _ui_compute(ref_text, hyp_text, norm):
58
+ wer, cer = compute_wer_cer(ref_text or "", hyp_text or "", norm)
59
+ return wer, cer
60
+
61
+ btn_calc.click(
62
+ _ui_compute,
63
+ inputs=[ref, hyp, normalize],
64
+ outputs=[out_wer, out_cer],
65
+ )
66
 
67
  with gr.Tab("📊 Датасеты"):
68
+ gr.HTML(get_datasets_description())
69
 
70
  with gr.Tab("✉️ Отправить результат"):
71
+ gr.HTML(get_submit_html())
72
+ with gr.Row():
73
+ with gr.Column():
74
+ model_name = gr.Textbox(
75
+ label="Название модели *", placeholder="MyAwesomeASRModel"
76
+ )
77
+ link = gr.Textbox(
78
+ label="Ссылка на модель *",
79
+ placeholder="https://huggingface.co/username/model",
80
+ )
81
+ license_field = gr.Textbox(
82
+ label="Лицензия *", placeholder="MIT / Apache-2.0 / Closed"
83
+ )
84
+ with gr.Column():
85
+ metrics_json = gr.TextArea(
86
+ label="Метрики JSON *",
87
+ placeholder='{"Russian_LibriSpeech": {"wer": 0.1234, "cer": 0.0567}, ...}',
88
+ lines=16,
89
+ )
90
+
91
+ submit_btn = gr.Button("🚀 Отправить", elem_classes="full-width-btn")
92
+ output_msg = gr.HTML()
93
+
94
+ def _alert(kind, text):
95
+ return f'<div class="alert {kind}">{text}</div>'
96
+
97
+ def build_json_and_submit(name, link_, lic, metrics_str):
98
+ name = (name or "").strip()
99
+ link_ = (link_ or "").strip()
100
+ lic = (lic or "").strip()
101
+ if not name:
102
+ return (
103
+ gr.update(),
104
+ _alert("error", "Укажите название модели."),
105
+ metrics_str,
106
+ )
107
+ if not link_ or not (
108
+ link_.startswith("http://") or link_.startswith("https://")
109
+ ):
110
+ return (
111
+ gr.update(),
112
+ _alert(
113
+ "error", "Ссылка должна начинаться с http:// или https://"
114
+ ),
115
+ metrics_str,
116
+ )
117
+ if not lic:
118
+ return (
119
+ gr.update(),
120
+ _alert("error", "Укажите лицензию модели."),
121
+ metrics_str,
122
+ )
123
+ try:
124
+ metrics = json.loads(metrics_str)
125
+ except Exception as e:
126
+ return (
127
+ gr.update(),
128
+ _alert("error", f"Невалидный JSON метрик: {e}"),
129
+ metrics_str,
130
+ )
131
+ if not isinstance(metrics, dict):
132
+ return (
133
+ gr.update(),
134
+ _alert(
135
+ "error",
136
+ "Метрики должны быть объектом JSON с датасетами верхнего уровня.",
137
+ ),
138
+ metrics_str,
139
+ )
140
+ missing = [ds for ds in DATASETS if ds not in metrics]
141
+ extra = [k for k in metrics.keys() if k not in DATASETS]
142
+ if missing:
143
+ return (
144
+ gr.update(),
145
+ _alert("error", f"Отсутствуют датасеты: {', '.join(missing)}"),
146
+ metrics_str,
147
+ )
148
+ if extra:
149
+ return (
150
+ gr.update(),
151
+ _alert("error", f"Лишние ключи в метриках: {', '.join(extra)}"),
152
+ metrics_str,
153
+ )
154
+ for ds in DATASETS:
155
+ entry = metrics.get(ds)
156
+ if not isinstance(entry, dict):
157
+ return (
158
+ gr.update(),
159
+ _alert(
160
+ "error",
161
+ f"{ds}: значение должно быть объектом с полями wer и cer",
162
+ ),
163
+ metrics_str,
164
+ )
165
+ for k in ("wer", "cer"):
166
+ v = entry.get(k)
167
+ if not isinstance(v, (int, float)):
168
+ return (
169
+ gr.update(),
170
+ _alert("error", f"{ds}: поле {k} должно быть числом"),
171
+ metrics_str,
172
+ )
173
+ if not (0 <= float(v) <= 1):
174
+ return (
175
+ gr.update(),
176
+ _alert(
177
+ "error",
178
+ f"{ds}: поле {k} должно быть в диапазоне [0, 1]",
179
+ ),
180
+ metrics_str,
181
+ )
182
+ payload = json.dumps(
183
+ {
184
+ "model_name": name,
185
+ "link": link_,
186
+ "license": lic,
187
+ "metrics": metrics,
188
+ },
189
+ ensure_ascii=False,
190
+ )
191
+ updated_html, status_msg, cleared = process_submit(payload)
192
+ if updated_html is None:
193
+ msg = status_msg.replace("Ошибка:", "").strip()
194
+ return (
195
+ gr.update(),
196
+ _alert("error", f"Не удалось добавить: {msg}"),
197
+ metrics_str,
198
+ )
199
+ return (
200
+ updated_html,
201
+ _alert("success", "✅ Результат добавлен в лидерборд."),
202
+ "",
203
+ )
204
+
205
  submit_btn.click(
206
+ build_json_and_submit,
207
+ inputs=[model_name, link, license_field, metrics_json],
208
+ outputs=[leaderboard_html, output_msg, metrics_json],
209
  )
210
 
211
  demo.launch()
constants.py CHANGED
@@ -1,78 +1,13 @@
1
  import os
2
 
3
  INTRODUCTION_TEXT = """
4
- # Русский ASR Лидерборд
5
- Добро пожаловать в лидерборд для моделей автоматического распознавания речи (ASR) на русском языке.
6
- Здесь вы можете сравнить производительность различных моделей по метрикам WER (Word Error Rate) и CER (Character Error Rate) на нескольких датасетах.
7
- Лидерборд сортируется по среднему WER (⬇️ - чем ниже, тем лучше).
8
- Наведите курсор на значение WER в колонке датасета, чтобы увидеть CER.
9
- Все метрики указаны в процентах (%).
10
  """
11
 
12
- METRICS_TAB_TEXT = """
13
- # Метрики
14
- Метрики рассчитываются на текстах в нижнем регистре и без пунктуации.
15
-
16
- - **WER (Word Error Rate)**: Ошибка на уровне слов. Рассчитывается как:
17
-
18
- $$ WER = \\frac{S + D + I}{N} $$
19
-
20
- где S - количество замен, D - удалений, I - вставок, N - количество слов в референсе.
21
-
22
- - **CER (Character Error Rate)**: Ошибка на уровне символов. Аналогичная формула, но для символов:
23
-
24
- $$ CER = \\frac{S + D + I}{N} $$
25
-
26
- где S, D, I, N - соответственно замены, удаления, вставки и количество символов в референсе.
27
-
28
- - **Средние значения**: Простое среднее по всем датасетам.
29
- - Все метрики нормализованы и представлены в процентах для удобства сравнения.
30
- """
31
-
32
- SUBMIT_TAB_TEXT = """
33
- # Отправить результат
34
- Чтобы добавить вашу модель в лидерборд, отправьте JSON с результатами. Метрики должны быть в диапазоне [0, 1] (не в процентах).
35
-
36
- Формат:
37
- ```json
38
- {
39
- "model_name": "MyAwesomeASRModel",
40
- "link": "https://huggingface.co/myusername/my-asr-model",
41
- "license": "Apache-2.0",
42
- "metrics": {
43
- "Russian_LibriSpeech": {
44
- "wer": 0.1234,
45
- "cer": 0.0567
46
- },
47
- "Common_Voice_Corpus_22.0": {
48
- "wer": 0.2345,
49
- "cer": 0.0789
50
- },
51
- "Tone_Webinars": {
52
- "wer": 0.3456,
53
- "cer": 0.0987
54
- },
55
- "Tone_Books": {
56
- "wer": 0.4567,
57
- "cer": 0.1098
58
- },
59
- "Tone_Speak": {
60
- "wer": 0.5678,
61
- "cer": 0.1209
62
- },
63
- "Sova_RuDevices": {
64
- "wer": 0.6789,
65
- "cer": 0.1310
66
- }
67
- }
68
- }
69
- ```
70
-
71
- В отчёте обязательно должны быть все датасеты, а именно: Russian_LibriSpeech, Common_Voice_Corpus_22.0, Tone_Webinars, Tone_Books, Tone_Speak, Sova_RuDevices.
72
- После отправки лидерборд обновится автоматически.
73
- """
74
  REPO_ID = "Vikhrmodels/russian-asr-leaderboard"
75
  HF_TOKEN = os.getenv("HF_TOKEN")
 
76
  DATASETS = [
77
  "Russian_LibriSpeech",
78
  "Common_Voice_Corpus_22.0",
@@ -81,36 +16,38 @@ DATASETS = [
81
  "Tone_Speak",
82
  "Sova_RuDevices",
83
  ]
 
84
  SHORT_DATASET_NAMES = ["RuLS", "CV 22.0", "Webinars", "Books", "Speak", "Sova"]
 
85
  DATASET_DESCRIPTIONS = {
86
  "RuLS": {
87
  "full_name": "Russian_LibriSpeech",
88
- "description": "Russian LibriSpeech (RuLS) — датасет на основе аудиокниг из общественного достояния от LibriVox, содержащий около 98 часов русской речи с транскрипциями.",
89
  "num_rows": 1352,
90
  },
91
  "CV 22.0": {
92
  "full_name": "Common_Voice_Corpus_22.0",
93
- "description": "Common Voice — краудсорсинговый многоязычный корпус речи от Mozilla. Версия 22.0 включает данные русской речи с транскрипциями.",
94
  "num_rows": 10244,
95
  },
96
  "Webinars": {
97
  "full_name": "Tone_Webinars",
98
- "description": "Tone_Webinars датасет русской речи из вебинаров с транскрипциями.",
99
  "num_rows": 21587,
100
  },
101
  "Books": {
102
  "full_name": "Tone_Books",
103
- "description": "Tone_Books датасет русских аудиокниг с транскрипциями.",
104
- "num_rows": 4938,
105
  },
106
  "Speak": {
107
  "full_name": "Tone_Speak",
108
- "description": "Tone_Speak датасет синтетической русской речи с транскрипциями.",
109
  "num_rows": 700,
110
  },
111
  "Sova": {
112
  "full_name": "Sova_RuDevices",
113
- "description": "SOVA RuDevices — акустический корпус примерно 100 часов 16kHz живой русской речи, записанной на устройствах, с ручной транскрипцией.",
114
  "num_rows": 5799,
115
  },
116
  }
 
1
  import os
2
 
3
  INTRODUCTION_TEXT = """
4
+ # Русский ASR-лидерборд
5
+ Площадка для честного сравнения моделей распознавания русской речи. Мы считаем WER и CER на единых тестовых наборах и сортируем модели по среднему WER (ниже — лучше). Наведите курсор на значение WER в колонке датасета, чтобы увидеть CER. Все метрики указаны в процентах.
 
 
 
 
6
  """
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  REPO_ID = "Vikhrmodels/russian-asr-leaderboard"
9
  HF_TOKEN = os.getenv("HF_TOKEN")
10
+
11
  DATASETS = [
12
  "Russian_LibriSpeech",
13
  "Common_Voice_Corpus_22.0",
 
16
  "Tone_Speak",
17
  "Sova_RuDevices",
18
  ]
19
+
20
  SHORT_DATASET_NAMES = ["RuLS", "CV 22.0", "Webinars", "Books", "Speak", "Sova"]
21
+
22
  DATASET_DESCRIPTIONS = {
23
  "RuLS": {
24
  "full_name": "Russian_LibriSpeech",
25
+ "description": "Корпус на основе русскоязычных аудиокниг LibriVox. Около 98 часов речи с верифицированными транскрипциями.",
26
  "num_rows": 1352,
27
  },
28
  "CV 22.0": {
29
  "full_name": "Common_Voice_Corpus_22.0",
30
+ "description": "Краудсорсинговый многоязычный корпус Mozilla Common Voice. Версия 22.0 содержит русскую речь с транскрипциями.",
31
  "num_rows": 10244,
32
  },
33
  "Webinars": {
34
  "full_name": "Tone_Webinars",
35
+ "description": "Речь из образовательных вебинаров. Разнообразные дикторы и темы, близкие к реальным сценариям.",
36
  "num_rows": 21587,
37
  },
38
  "Books": {
39
  "full_name": "Tone_Books",
40
+ "description": "Фрагменты русских аудиокниг. Чистая дикторская речь и аккуратные транскрипции.",
41
+ "num_rows": 4930,
42
  },
43
  "Speak": {
44
  "full_name": "Tone_Speak",
45
+ "description": "Синтетическая русская речь. Полезна для оценки устойчивости к TTS-голосам.",
46
  "num_rows": 700,
47
  },
48
  "Sova": {
49
  "full_name": "Sova_RuDevices",
50
+ "description": "Около 100 часов живой русской речи, записанной на устройствах 16 kHz. Тщательно размеченные транскрипции.",
51
  "num_rows": 5799,
52
  },
53
  }
styles.py CHANGED
@@ -1,23 +1,72 @@
1
  LEADERBOARD_CSS = """
2
- .leaderboard-wrapper { overflow-x: auto; -ms-overflow-style: none; scrollbar-width: none; margin-bottom: 40px; }
3
- .leaderboard-wrapper::-webkit-scrollbar { display: none; }
4
- .leaderboard-table { min-width: 100%; }
5
- .leaderboard-table table { border-collapse: collapse; width: 100%; }
6
- .leaderboard-table th, .leaderboard-table td { border: 1px solid #ddd; padding: 8px; text-align: center; }
7
- .leaderboard-table th { background-color: #f2f2f2; font-weight: bold; }
8
- .leaderboard-table tr:nth-child(even) { background-color: #f9f9f9; }
9
- .leaderboard-table tr:hover { background-color: #f1f1f1; }
10
  .leaderboard-table a { color: #0366d6; text-decoration: none; }
11
  .leaderboard-table a:hover { text-decoration: underline; }
12
- .leaderboard-table span { cursor: pointer; }
13
- /* Dark mode */
 
 
 
 
14
  .dark .leaderboard-table th, .dark .leaderboard-table td { border-color: #30363d; color: #e0e0e0; }
15
  .dark .leaderboard-table th { background-color: #21262d; }
16
- .dark .leaderboard-table tr:nth-child(even) { background-color: #161b22; }
17
- .dark .leaderboard-table tr:hover { background-color: #0d1117; }
18
  .dark .leaderboard-table a { color: #58a6ff; }
19
- /* Other CSS */
 
20
  .gradio-container { max-width: 1400px; margin: auto; padding: 20px; }
21
  .markdown-text { color: #24292e; padding: 15px; border-radius: 6px; background-color: #f6f8fa; margin-bottom: 20px; }
22
  .dark .markdown-text { color: #c9d1d9; background-color: #161b22; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  """
 
1
  LEADERBOARD_CSS = """
2
+ /* ====== Leaderboard ====== */
3
+ .leaderboard-wrapper { overflow-x: auto; margin-bottom: 40px; }
4
+ .leaderboard-table table { width: 100%; border-collapse: collapse; }
5
+ .leaderboard-table th, .leaderboard-table td { text-align: center; padding: 8px; }
 
 
 
 
6
  .leaderboard-table a { color: #0366d6; text-decoration: none; }
7
  .leaderboard-table a:hover { text-decoration: underline; }
8
+ .metric-cell { cursor: help; display:inline-block; padding:2px 6px; border-radius: 8px; }
9
+ .best-metric { position: relative; background: rgba(88,166,255,.16); box-shadow: inset 0 0 0 1px rgba(88,166,255,.35); font-weight: 600; }
10
+ .best-metric:before { content: "★"; margin-right: 6px; font-size: 0.9em; color: #3b82f6; }
11
+ .dark .best-metric { background: rgba(88,166,255,.28); box-shadow: inset 0 0 0 1px rgba(88,166,255,.5); }
12
+ .dark .best-metric:before { color: #58a6ff; }
13
+
14
  .dark .leaderboard-table th, .dark .leaderboard-table td { border-color: #30363d; color: #e0e0e0; }
15
  .dark .leaderboard-table th { background-color: #21262d; }
 
 
16
  .dark .leaderboard-table a { color: #58a6ff; }
17
+
18
+ /* ====== Container & Markdown ====== */
19
  .gradio-container { max-width: 1400px; margin: auto; padding: 20px; }
20
  .markdown-text { color: #24292e; padding: 15px; border-radius: 6px; background-color: #f6f8fa; margin-bottom: 20px; }
21
  .dark .markdown-text { color: #c9d1d9; background-color: #161b22; }
22
+
23
+ /* ====== Dataset cards ====== */
24
+ .datasets-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; }
25
+ .dataset-card { background: #f6f8fa; border-radius: 8px; padding: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: transform .2s ease; }
26
+ .dataset-card:hover { transform: translateY(-4px); }
27
+ .dataset-card h3 { margin: 0 0 8px; color: #0366d6; }
28
+ .dataset-card .full-name { font-size: .85em; color: #4b5563; }
29
+ .dataset-card p { margin: 5px 0; }
30
+ .dataset-card .records { display:inline-block; padding: 2px 10px; border-radius: 999px; background: #eaf2ff; color: #0b63ce; font-weight: 600; }
31
+ .dark .dataset-card { background: #161b22; color: #c9d1d9; }
32
+ .dark .dataset-card h3 { color: #58a6ff; }
33
+ .dark .dataset-card .full-name { color: #a9c4e2; }
34
+ .dark .dataset-card .records { background: #0f2a45; color: #9bd1ff; }
35
+
36
+ /* ====== Metrics cards ====== */
37
+ .metrics-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; margin-bottom: 16px; }
38
+ .metric-card { background: #f6f8fa; border-radius: 12px; padding: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.04); color:#1f2937; }
39
+ .metric-card h3 { margin: 0 0 10px; color:#0b63ce; }
40
+ .metric-text { margin: 6px 0 0; }
41
+ .dark .metric-card { background:#161b22; color:#c9d1d9; }
42
+ .dark .metric-card h3 { color:#9bd1ff; }
43
+
44
+ .formula {
45
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
46
+ font-size: 15px; border-radius: 8px; padding: 8px 10px;
47
+ background: #eef3ff; color:#0b2a55; display: inline-block;
48
+ }
49
+ .formula span { font-weight: 700; }
50
+ .dark .formula { background: #0f1f33; color:#deecff; }
51
+
52
+ .chips { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; margin-top: 10px; }
53
+ .chip { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 10px; background: #ffffff; border: 1px solid #e5e7eb; color:#111827; }
54
+ .chip b { font-size: 13px; }
55
+ .chip small { font-size: 12px; opacity: .9; }
56
+ .dark .chip { background: #0f172a; border-color: #22304a; color:#e5e7eb; }
57
+
58
+ /* ====== Submit form ====== */
59
+ .submit-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
60
+ .form-card { background: #f6f8fa; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
61
+ .form-card h3 { margin-top: 0; color: #0366d6; }
62
+ .dark .form-card { background: #161b22; color: #c9d1d9; }
63
+ .dark .form-card h3 { color: #58a6ff; }
64
+ @media (max-width: 900px) { .submit-grid { grid-template-columns: 1fr; } }
65
+
66
+ /* ====== Alerts ====== */
67
+ .alert { padding:12px 14px; border-radius:8px; margin-top:10px; font-weight:500; }
68
+ .alert.success { background:#e6f7ed; color:#0f5132; border:1px solid #b7ebc6; }
69
+ .dark .alert.success { background:#0f2a1d; color:#a6f3c2; border-color:#1f5c3a; }
70
+ .alert.error { background:#fdecea; color:#842029; border:1px solid #f5c2c7; }
71
+ .dark .alert.error { background:#3a0b0e; color:#f5a3aa; border-color:#7a1a21; }
72
  """
utils.py CHANGED
@@ -4,6 +4,7 @@ from statistics import mean
4
  from huggingface_hub import HfApi, create_repo
5
  from datasets import load_dataset, Dataset
6
  from datasets.data_files import EmptyDatasetError
 
7
 
8
  from constants import (
9
  REPO_ID,
@@ -38,16 +39,19 @@ def load_data():
38
  if not df.empty:
39
  df = df.sort_values("overall_wer").reset_index(drop=True)
40
  df.insert(0, "rank", df.index + 1)
 
 
 
 
 
 
41
 
42
- df["overall_wer"] = (df["overall_wer"] * 100).round(2).apply(lambda x: f"{x}")
43
- df["overall_cer"] = (df["overall_cer"] * 100).round(2).apply(lambda x: f"{x}")
44
- for ds in DATASETS:
45
- df[f"wer_{ds}"] = (df[f"wer_{ds}"] * 100).round(2)
46
- df[f"cer_{ds}"] = (df[f"cer_{ds}"] * 100).round(2)
47
-
48
  for short_ds, ds in zip(SHORT_DATASET_NAMES, DATASETS):
49
  df[short_ds] = df.apply(
50
- lambda row: f'<span title="CER: {row[f"cer_{ds}"]:.2f}" style="cursor: help;">{row[f"wer_{ds}"]:.2f}</span>',
 
 
51
  axis=1,
52
  )
53
  df = df.drop(columns=[f"wer_{ds}", f"cer_{ds}"])
@@ -56,7 +60,6 @@ def load_data():
56
  lambda row: f'<a href="{row["link"]}" target="_blank">{row["model_name"]}</a>',
57
  axis=1,
58
  )
59
-
60
  df = df.drop(columns=["link"])
61
 
62
  df["license"] = df["license"].apply(
@@ -67,6 +70,10 @@ def load_data():
67
  else "Закрытая"
68
  )
69
 
 
 
 
 
70
  df.rename(
71
  columns={
72
  "overall_wer": "Средний WER ⬇️",
@@ -78,8 +85,24 @@ def load_data():
78
  inplace=True,
79
  )
80
 
81
- table_html = df.to_html(escape=False, index=False)
82
- return f'<div class="leaderboard-wrapper"><div class="leaderboard-table">{table_html}</div></div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  else:
84
  return (
85
  '<div class="leaderboard-wrapper"><div class="leaderboard-table"><table><thead><tr><th>Ранг</th><th>Модель</th><th>Тип модели</th><th>Средний WER ⬇️</th><th>Средний CER ⬇️</th>'
@@ -96,21 +119,17 @@ def process_submit(json_str):
96
  )
97
  try:
98
  data = json.loads(json_str)
99
-
100
  required_keys = ["model_name", "link", "license", "metrics"]
101
  if not all(key in data for key in required_keys):
102
  raise ValueError(
103
  "Неверная структура JSON. Требуемые поля: model_name, link, license, metrics"
104
  )
105
-
106
  metrics = data["metrics"]
107
  if set(metrics.keys()) != set(DATASETS):
108
  raise ValueError(
109
  f"Метрики должны быть для всех датасетов: {', '.join(DATASETS)}"
110
  )
111
-
112
- wers = []
113
- cers = []
114
  row = {
115
  "model_name": data["model_name"],
116
  "link": data["link"],
@@ -123,7 +142,6 @@ def process_submit(json_str):
123
  row[f"cer_{ds}"] = metrics[ds]["cer"]
124
  wers.append(metrics[ds]["wer"])
125
  cers.append(metrics[ds]["cer"])
126
-
127
  row["overall_wer"] = mean(wers)
128
  row["overall_cer"] = mean(cers)
129
 
@@ -132,6 +150,7 @@ def process_submit(json_str):
132
  df = dataset["train"].to_pandas()
133
  except EmptyDatasetError:
134
  df = pd.DataFrame(columns=columns)
 
135
  new_df = pd.concat([df, pd.DataFrame([row])], ignore_index=True)
136
  new_dataset = Dataset.from_pandas(new_df)
137
  new_dataset.push_to_hub(REPO_ID, token=HF_TOKEN)
@@ -143,7 +162,111 @@ def process_submit(json_str):
143
 
144
 
145
  def get_datasets_description():
146
- desc = "# Описание датасетов\n\n"
147
  for short_ds, info in DATASET_DESCRIPTIONS.items():
148
- desc += f"### {short_ds} ({info['full_name']})\n{info['description']}\n- Количество записей: {info['num_rows']}\n\n"
149
- return desc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from huggingface_hub import HfApi, create_repo
5
  from datasets import load_dataset, Dataset
6
  from datasets.data_files import EmptyDatasetError
7
+ import re
8
 
9
  from constants import (
10
  REPO_ID,
 
39
  if not df.empty:
40
  df = df.sort_values("overall_wer").reset_index(drop=True)
41
  df.insert(0, "rank", df.index + 1)
42
+ for col in (
43
+ ["overall_wer", "overall_cer"]
44
+ + [f"wer_{ds}" for ds in DATASETS]
45
+ + [f"cer_{ds}" for ds in DATASETS]
46
+ ):
47
+ df[col] = (df[col] * 100).round(2)
48
 
49
+ best_values = {ds: df[f"wer_{ds}"].min() for ds in DATASETS}
 
 
 
 
 
50
  for short_ds, ds in zip(SHORT_DATASET_NAMES, DATASETS):
51
  df[short_ds] = df.apply(
52
+ lambda row: f'<span title="CER: {row[f"cer_{ds}"]:.2f}%" '
53
+ f'class="metric-cell{" best-metric" if row[f"wer_{ds}"] == best_values[ds] else ""}">'
54
+ f"{row[f'wer_{ds}']:.2f}%</span>",
55
  axis=1,
56
  )
57
  df = df.drop(columns=[f"wer_{ds}", f"cer_{ds}"])
 
60
  lambda row: f'<a href="{row["link"]}" target="_blank">{row["model_name"]}</a>',
61
  axis=1,
62
  )
 
63
  df = df.drop(columns=["link"])
64
 
65
  df["license"] = df["license"].apply(
 
70
  else "Закрытая"
71
  )
72
 
73
+ df["rank"] = df["rank"].apply(
74
+ lambda r: "🥇" if r == 1 else "🥈" if r == 2 else "🥉" if r == 3 else str(r)
75
+ )
76
+
77
  df.rename(
78
  columns={
79
  "overall_wer": "Средний WER ⬇️",
 
85
  inplace=True,
86
  )
87
 
88
+ table_html = df.to_html(
89
+ escape=False, index=False, classes="display cell-border compact stripe"
90
+ )
91
+ scripts = """
92
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css">
93
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
94
+ <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
95
+ <script>
96
+ $(document).ready(function(){
97
+ $('.leaderboard-table table').DataTable({
98
+ pageLength: 25,
99
+ order: [[3, 'asc']],
100
+ language: { url: '//cdn.datatables.net/plug-ins/1.13.4/i18n/ru.json' }
101
+ });
102
+ });
103
+ </script>
104
+ """
105
+ return f'<div class="leaderboard-wrapper"><div class="leaderboard-table">{table_html}</div></div>{scripts}'
106
  else:
107
  return (
108
  '<div class="leaderboard-wrapper"><div class="leaderboard-table"><table><thead><tr><th>Ранг</th><th>Модель</th><th>Тип модели</th><th>Средний WER ⬇️</th><th>Средний CER ⬇️</th>'
 
119
  )
120
  try:
121
  data = json.loads(json_str)
 
122
  required_keys = ["model_name", "link", "license", "metrics"]
123
  if not all(key in data for key in required_keys):
124
  raise ValueError(
125
  "Неверная структура JSON. Требуемые поля: model_name, link, license, metrics"
126
  )
 
127
  metrics = data["metrics"]
128
  if set(metrics.keys()) != set(DATASETS):
129
  raise ValueError(
130
  f"Метрики должны быть для всех датасетов: {', '.join(DATASETS)}"
131
  )
132
+ wers, cers = [], []
 
 
133
  row = {
134
  "model_name": data["model_name"],
135
  "link": data["link"],
 
142
  row[f"cer_{ds}"] = metrics[ds]["cer"]
143
  wers.append(metrics[ds]["wer"])
144
  cers.append(metrics[ds]["cer"])
 
145
  row["overall_wer"] = mean(wers)
146
  row["overall_cer"] = mean(cers)
147
 
 
150
  df = dataset["train"].to_pandas()
151
  except EmptyDatasetError:
152
  df = pd.DataFrame(columns=columns)
153
+
154
  new_df = pd.concat([df, pd.DataFrame([row])], ignore_index=True)
155
  new_dataset = Dataset.from_pandas(new_df)
156
  new_dataset.push_to_hub(REPO_ID, token=HF_TOKEN)
 
162
 
163
 
164
  def get_datasets_description():
165
+ html = '<div class="datasets-container">'
166
  for short_ds, info in DATASET_DESCRIPTIONS.items():
167
+ html += f"""
168
+ <div class="dataset-card">
169
+ <h3>{short_ds} <span class="full-name">{info["full_name"]}</span></h3>
170
+ <p>{info["description"]}</p>
171
+ <p class="records">📊 {info["num_rows"]} записей</p>
172
+ </div>
173
+ """
174
+ html += "</div>"
175
+ return html
176
+
177
+
178
+ def _strip_punct(text: str) -> str:
179
+ return re.sub(r"[^\w\s]+", "", text, flags=re.UNICODE)
180
+
181
+
182
+ def normalize_text(s: str) -> str:
183
+ return _strip_punct(s.lower()).strip()
184
+
185
+
186
+ def _edit_distance(a, b):
187
+ n, m = len(a), len(b)
188
+ dp = [[0] * (m + 1) for _ in range(n + 1)]
189
+ for i in range(n + 1):
190
+ dp[i][0] = i
191
+ for j in range(m + 1):
192
+ dp[0][j] = j
193
+ for i in range(1, n + 1):
194
+ ai = a[i - 1]
195
+ for j in range(1, m + 1):
196
+ cost = 0 if ai == b[j - 1] else 1
197
+ dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)
198
+ return dp[n][m]
199
+
200
+
201
+ def compute_wer_cer(ref: str, hyp: str, normalize: bool = True):
202
+ if normalize:
203
+ ref_norm, hyp_norm = normalize_text(ref), normalize_text(hyp)
204
+ else:
205
+ ref_norm, hyp_norm = ref, hyp
206
+ ref_words, hyp_words = ref_norm.split(), hyp_norm.split()
207
+ Nw = max(1, len(ref_words))
208
+ wer = _edit_distance(ref_words, hyp_words) / Nw
209
+ ref_chars, hyp_chars = list(ref_norm), list(hyp_norm)
210
+ Nc = max(1, len(ref_chars))
211
+ cer = _edit_distance(ref_chars, hyp_chars) / Nc
212
+ return round(wer * 100, 2), round(cer * 100, 2)
213
+
214
+
215
+ def get_metrics_html():
216
+ return """
217
+ <div class="metrics-grid">
218
+ <div class="metric-card">
219
+ <h3>WER — Word Error Rate</h3>
220
+ <div class="formula">WER = ( <span>S</span> + <span>D</span> + <span>I</span> ) / <span>N</span></div>
221
+ <div class="chips">
222
+ <div class="chip"><b>S</b><small>замены</small></div>
223
+ <div class="chip"><b>D</b><small>удаления</small></div>
224
+ <div class="chip"><b>I</b><small>вставки</small></div>
225
+ <div class="chip"><b>N</b><small>слов в референсе</small></div>
226
+ </div>
227
+ </div>
228
+ <div class="metric-card">
229
+ <h3>CER — Character Error Rate</h3>
230
+ <div class="formula">CER = ( <span>S</span> + <span>D</span> + <span>I</span> ) / <span>N</span></div>
231
+ <div class="chips">
232
+ <div class="chip"><b>S, D, I</b><small>операции редактирования</small></div>
233
+ <div class="chip"><b>N</b><small>символов в референсе</small></div>
234
+ </div>
235
+ </div>
236
+ <div class="metric-card">
237
+ <h3>Нормализация</h3>
238
+ <p class="metric-text">Перед расчётом приводим текст к нижнему регистру и удаляем пунктуацию.</p>
239
+ </div>
240
+ <div class="metric-card">
241
+ <h3>Сравнение</h3>
242
+ <p class="metric-text">Сортировка по среднему WER по всем датасетам. Метрики отображаются в процентах.</p>
243
+ </div>
244
+ </div>
245
+ """
246
+
247
+
248
+ def get_submit_html():
249
+ return """
250
+ <div class="submit-grid">
251
+ <div class="form-card">
252
+ <h3>Общая информация</h3>
253
+ <ul>
254
+ <li><b>Название модели</b> — коротко и понятно.</li>
255
+ <li><b>Ссылка</b> — HuggingFace, GitHub или сайт.</li>
256
+ <li><b>Лицензия</b> — MIT, Apache-2.0, GPL или Closed.</li>
257
+ </ul>
258
+ </div>
259
+ <div class="form-card">
260
+ <h3>Метрики</h3>
261
+ <p>Укажите WER и CER для всех датасетов в формате JSON. Значения — от 0 до 1.</p>
262
+ <pre>{
263
+ "Russian_LibriSpeech": {"wer": 0.1234, "cer": 0.0567},
264
+ "Common_Voice_Corpus_22.0": {"wer": 0.2345, "cer": 0.0789},
265
+ "Tone_Webinars": {"wer": 0.3456, "cer": 0.0987},
266
+ "Tone_Books": {"wer": 0.4567, "cer": 0.1098},
267
+ "Tone_Speak": {"wer": 0.5678, "cer": 0.1209},
268
+ "Sova_RuDevices": {"wer": 0.6789, "cer": 0.1310}
269
+ }</pre>
270
+ </div>
271
+ </div>
272
+ """