eternalGenius commited on
Commit
0d5e279
·
verified ·
1 Parent(s): 69326dc

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +30 -7
  2. app.py +374 -0
  3. requirements.txt +12 -0
README.md CHANGED
@@ -1,14 +1,37 @@
1
  ---
2
- title: TestASRspace
3
- emoji: 🦀
4
- colorFrom: pink
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Russian ASR Benchmark
3
+ emoji: 🎙️
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: 5.29.0
 
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
  ---
12
 
13
+ # Russian ASR Benchmark
14
+
15
+ Hugging Face Space для сравнения двух моделей распознавания речи:
16
+
17
+ - `Sh1man/whisper-large-v3-russian-ties-podlodka-v1.2-ct`
18
+ - `ai-sage/GigaAM-v3` с revision `e2e_rnnt`
19
+
20
+ Что умеет:
21
+
22
+ - загрузка аудиофайла;
23
+ - ввод эталонного текста вручную или загрузка `.txt`;
24
+ - транскрибация обеими моделями в максимально близких к целевому инференсу конфигурациях;
25
+ - расчёт `WER` и `CER`;
26
+ - встроенный `GigaAM transcribe_longform` для длинных записей.
27
+
28
+ ## Notes
29
+
30
+ - Первая загрузка будет долгой: Space скачивает веса моделей.
31
+ - Для `GigaAM v3 e2e_rnnt` используется revision `e2e_rnnt` репозитория `ai-sage/GigaAM-v3`, как указано в model card.
32
+ - Для `GigaAM transcribe_longform` нужен секрет `HF_TOKEN` в настройках Space и принятые условия доступа к [`pyannote/segmentation-3.0`](https://huggingface.co/pyannote/segmentation-3.0).
33
+ - `Whisper` использует `faster-whisper` / CTranslate2 с моделью `Sh1man/whisper-large-v3-russian-ties-podlodka-v1.2-ct`.
34
+ - Для `Whisper` включён `BatchedInferencePipeline`, используется VAD по умолчанию и `beam_size=5`.
35
+ - Word timestamps и дополнительный alignment для `Whisper` не используются, чтобы не замедлять инференс.
36
+ - `GigaAM` использует встроенный VAD-longform через `transcribe_longform`.
37
+ - Метрики можно считать как в сыром виде, так и после нормализации текста.
app.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import gc
4
+ import os
5
+ import re
6
+ import tempfile
7
+ import time
8
+ import unicodedata
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import gradio as gr
13
+ import torch
14
+ import torchaudio
15
+ from faster_whisper import BatchedInferencePipeline, WhisperModel
16
+ from jiwer import cer, wer
17
+ from transformers import AutoModel
18
+
19
+ WHISPER_MODEL_ID = "Sh1man/whisper-large-v3-russian-ties-podlodka-v1.2-ct"
20
+ GIGAAM_MODEL_ID = "ai-sage/GigaAM-v3"
21
+ GIGAAM_REVISION = "e2e_rnnt"
22
+
23
+ TARGET_SAMPLE_RATE = 16_000
24
+ WHISPER_BEAM_SIZE = 5
25
+ WHISPER_BATCH_SIZE = 8 if torch.cuda.is_available() else 4
26
+ WHISPER_DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
27
+ WHISPER_COMPUTE_TYPE = "float16" if torch.cuda.is_available() else "int8"
28
+
29
+ MODEL_LABELS = {
30
+ "whisper": "Sh1man Whisper Large V3 CT",
31
+ "gigaam": "GigaAM v3 e2e RNNT",
32
+ }
33
+
34
+ MODEL_STATE: dict[str, Any] = {"name": None, "instance": None}
35
+
36
+
37
+ def cleanup_loaded_model() -> None:
38
+ loaded = MODEL_STATE.get("instance")
39
+ MODEL_STATE["name"] = None
40
+ MODEL_STATE["instance"] = None
41
+ if loaded is not None:
42
+ del loaded
43
+ gc.collect()
44
+ if torch.cuda.is_available():
45
+ torch.cuda.empty_cache()
46
+
47
+
48
+ def get_model(model_name: str) -> Any:
49
+ if MODEL_STATE["name"] == model_name and MODEL_STATE["instance"] is not None:
50
+ return MODEL_STATE["instance"]
51
+
52
+ cleanup_loaded_model()
53
+
54
+ if model_name == "whisper":
55
+ whisper_model = WhisperModel(
56
+ WHISPER_MODEL_ID,
57
+ device=WHISPER_DEVICE,
58
+ compute_type=WHISPER_COMPUTE_TYPE,
59
+ )
60
+ model = BatchedInferencePipeline(model=whisper_model)
61
+ elif model_name == "gigaam":
62
+ model = AutoModel.from_pretrained(
63
+ GIGAAM_MODEL_ID,
64
+ revision=GIGAAM_REVISION,
65
+ trust_remote_code=True,
66
+ )
67
+ if hasattr(model, "eval"):
68
+ model.eval()
69
+ if torch.cuda.is_available() and hasattr(model, "to"):
70
+ model = model.to("cuda")
71
+ else:
72
+ raise ValueError(f"Unsupported model name: {model_name}")
73
+
74
+ MODEL_STATE["name"] = model_name
75
+ MODEL_STATE["instance"] = model
76
+ return model
77
+
78
+
79
+ def collapse_spaces(text: str) -> str:
80
+ return " ".join(text.split())
81
+
82
+
83
+ def normalize_for_metrics(text: str, enabled: bool) -> str:
84
+ text = unicodedata.normalize("NFKC", text.strip())
85
+ if not enabled:
86
+ return collapse_spaces(text)
87
+
88
+ text = text.lower().replace("ё", "е")
89
+ text = re.sub(r"[^\w\s]", " ", text, flags=re.UNICODE)
90
+ text = text.replace("_", " ")
91
+ return collapse_spaces(text)
92
+
93
+
94
+ def extract_text(result: Any) -> str:
95
+ if isinstance(result, str):
96
+ return result
97
+
98
+ if isinstance(result, dict):
99
+ for key in ("text", "transcription", "prediction"):
100
+ value = result.get(key)
101
+ if isinstance(value, str):
102
+ return value
103
+ if "chunks" in result and isinstance(result["chunks"], list):
104
+ return " ".join(
105
+ extract_text(chunk) for chunk in result["chunks"] if chunk is not None
106
+ ).strip()
107
+
108
+ if isinstance(result, list):
109
+ return " ".join(extract_text(item) for item in result if item is not None).strip()
110
+
111
+ return str(result)
112
+
113
+
114
+ def prepare_audio_file(audio_path: str) -> tuple[tempfile.TemporaryDirectory, str, float]:
115
+ waveform, sample_rate = torchaudio.load(audio_path)
116
+ if waveform.shape[0] > 1:
117
+ waveform = waveform.mean(dim=0, keepdim=True)
118
+ if sample_rate != TARGET_SAMPLE_RATE:
119
+ waveform = torchaudio.functional.resample(waveform, sample_rate, TARGET_SAMPLE_RATE)
120
+
121
+ duration_seconds = waveform.shape[1] / TARGET_SAMPLE_RATE
122
+ temp_dir = tempfile.TemporaryDirectory()
123
+ prepared_audio_path = Path(temp_dir.name) / "prepared_audio.wav"
124
+ torchaudio.save(str(prepared_audio_path), waveform, TARGET_SAMPLE_RATE)
125
+ return temp_dir, str(prepared_audio_path), duration_seconds
126
+
127
+
128
+ def transcribe_with_whisper(prepared_audio_path: str) -> tuple[str, str]:
129
+ transcriber = get_model("whisper")
130
+ segments, _ = transcriber.transcribe(
131
+ prepared_audio_path,
132
+ batch_size=WHISPER_BATCH_SIZE,
133
+ beam_size=WHISPER_BEAM_SIZE,
134
+ language="ru",
135
+ word_timestamps=False,
136
+ )
137
+ transcription = collapse_spaces(" ".join(segment.text for segment in segments if segment.text))
138
+ mode_note = (
139
+ "Whisper использовал `faster-whisper` + `BatchedInferencePipeline` "
140
+ f"с VAD по умолчанию, `beam_size={WHISPER_BEAM_SIZE}`, "
141
+ f"`batch_size={WHISPER_BATCH_SIZE}`, `compute_type={WHISPER_COMPUTE_TYPE}`."
142
+ )
143
+ return transcription, mode_note
144
+
145
+
146
+ def format_boundary(boundary: Any) -> str:
147
+ if not isinstance(boundary, (tuple, list)) or len(boundary) != 2:
148
+ return ""
149
+ start, end = boundary
150
+ return f"[{start:.2f}-{end:.2f}]"
151
+
152
+
153
+ def extract_longform_text(result: Any) -> str:
154
+ if not isinstance(result, list):
155
+ return collapse_spaces(extract_text(result))
156
+
157
+ parts: list[str] = []
158
+ for segment in result:
159
+ if isinstance(segment, dict):
160
+ segment_text = extract_text(segment)
161
+ else:
162
+ segment_text = extract_text(segment)
163
+ if segment_text:
164
+ parts.append(collapse_spaces(segment_text))
165
+ return collapse_spaces(" ".join(parts))
166
+
167
+
168
+ def transcribe_with_gigaam(audio_path: str) -> tuple[str, int]:
169
+ if not os.getenv("HF_TOKEN"):
170
+ raise ValueError(
171
+ "Для GigaAM longform нужен секрет HF_TOKEN с доступом к "
172
+ "'pyannote/segmentation-3.0'. Добавь его в Settings -> Variables and secrets."
173
+ )
174
+
175
+ transcriber = get_model("gigaam")
176
+ with torch.inference_mode():
177
+ result = transcriber.transcribe_longform(audio_path)
178
+ return extract_longform_text(result), len(result) if isinstance(result, list) else 0
179
+
180
+
181
+ def load_reference_text(reference_text: str, reference_file: str | None) -> str:
182
+ if reference_text.strip():
183
+ return reference_text.strip()
184
+ if reference_file:
185
+ for encoding in ("utf-8", "utf-8-sig", "cp1251"):
186
+ try:
187
+ return Path(reference_file).read_text(encoding=encoding).strip()
188
+ except UnicodeDecodeError:
189
+ continue
190
+ raise ValueError("Не удалось прочитать эталонный текстовый файл.")
191
+ return ""
192
+
193
+
194
+ def format_metric(value: float | None) -> str:
195
+ if value is None:
196
+ return "n/a"
197
+ return f"{value:.4f}"
198
+
199
+
200
+ def benchmark_audio(
201
+ audio_path: str | None,
202
+ reference_text: str,
203
+ reference_file: str | None,
204
+ selected_models: list[str],
205
+ normalize_metrics: bool,
206
+ ) -> tuple[list[list[Any]], str, str, str]:
207
+ if not audio_path:
208
+ raise gr.Error("Загрузи аудиофайл для транскрибации.")
209
+ if not selected_models:
210
+ raise gr.Error("Выбери хотя бы одну модель.")
211
+
212
+ reference = load_reference_text(reference_text, reference_file)
213
+ normalized_reference = normalize_for_metrics(reference, normalize_metrics) if reference else ""
214
+
215
+ temporary_dir: tempfile.TemporaryDirectory | None = None
216
+ try:
217
+ temporary_dir, prepared_audio_path, duration_seconds = prepare_audio_file(audio_path)
218
+
219
+ whisper_text = "Модель не запускалась."
220
+ gigaam_text = "Модель не запускалась."
221
+ rows: list[list[Any]] = []
222
+ whisper_mode_note: str | None = None
223
+ gigaam_segment_count: int | None = None
224
+
225
+ for model_name in selected_models:
226
+ started_at = time.perf_counter()
227
+ if model_name == "whisper":
228
+ transcription, whisper_mode_note = transcribe_with_whisper(prepared_audio_path)
229
+ whisper_text = transcription or "Пустой результат."
230
+ elif model_name == "gigaam":
231
+ transcription, gigaam_segment_count = transcribe_with_gigaam(prepared_audio_path)
232
+ gigaam_text = transcription or "Пустой результат."
233
+ else:
234
+ continue
235
+
236
+ elapsed = time.perf_counter() - started_at
237
+ current_wer: float | None = None
238
+ current_cer: float | None = None
239
+
240
+ if normalized_reference:
241
+ normalized_prediction = normalize_for_metrics(transcription, normalize_metrics)
242
+ current_wer = wer(normalized_reference, normalized_prediction)
243
+ current_cer = cer(normalized_reference, normalized_prediction)
244
+
245
+ rows.append(
246
+ [
247
+ MODEL_LABELS[model_name],
248
+ format_metric(current_wer),
249
+ format_metric(current_cer),
250
+ round(elapsed, 2),
251
+ ]
252
+ )
253
+
254
+ summary_lines = [
255
+ f"- Длительность аудио: `{duration_seconds:.1f}` сек.",
256
+ ]
257
+ if whisper_mode_note is not None:
258
+ summary_lines.append(f"- {whisper_mode_note}")
259
+ if gigaam_segment_count is not None:
260
+ summary_lines.append(
261
+ f"- GigaAM использовал встроенный `transcribe_longform` и собрал `{gigaam_segment_count}` сегментов через VAD."
262
+ )
263
+ if reference:
264
+ normalization_note = "с нормализацией" if normalize_metrics else "без нормализации"
265
+ summary_lines.append(f"- `WER` и `CER` посчитаны {normalization_note}.")
266
+ else:
267
+ summary_lines.append("- Эталонный текст не задан, метрики пропущены.")
268
+
269
+ return rows, whisper_text, gigaam_text, "\n".join(summary_lines)
270
+ except Exception as error:
271
+ raise gr.Error(f"Ошибка обработки: {error}") from error
272
+ finally:
273
+ if temporary_dir is not None:
274
+ temporary_dir.cleanup()
275
+
276
+
277
+ with gr.Blocks(title="Russian ASR Benchmark Space") as demo:
278
+ gr.Markdown(
279
+ """
280
+ # Russian ASR Benchmark
281
+ Сравнение двух ASR-моделей:
282
+
283
+ - `Sh1man/whisper-large-v3-russian-ties-podlodka-v1.2-ct`
284
+ - `ai-sage/GigaAM-v3` c revision `e2e_rnnt`
285
+
286
+ Загрузи аудио, вставь эталонный текст или приложи `.txt`, и Space посчитает `WER` / `CER` для каждой модели.
287
+
288
+ Для `GigaAM` используется встроенный `transcribe_longform`. Для него нужен `HF_TOKEN`
289
+ в секретах Space с доступом к `pyannote/segmentation-3.0`.
290
+ """
291
+ )
292
+
293
+ with gr.Row():
294
+ audio_input = gr.Audio(
295
+ label="Аудиофайл",
296
+ type="filepath",
297
+ sources=["upload", "microphone"],
298
+ )
299
+ with gr.Column():
300
+ reference_input = gr.Textbox(
301
+ label="Эталонный текст",
302
+ placeholder="Вставь правильную расшифровку сюда",
303
+ lines=10,
304
+ )
305
+ reference_file_input = gr.File(
306
+ label="Или загрузи эталонный текст (.txt)",
307
+ file_types=[".txt"],
308
+ type="filepath",
309
+ )
310
+
311
+ with gr.Row():
312
+ model_selector = gr.CheckboxGroup(
313
+ label="Модели для запуска",
314
+ choices=[
315
+ ("Sh1man Whisper Large V3 CT", "whisper"),
316
+ ("GigaAM v3 e2e RNNT", "gigaam"),
317
+ ],
318
+ value=["whisper", "gigaam"],
319
+ )
320
+ normalize_checkbox = gr.Checkbox(
321
+ label="Нормализовать текст перед подсчётом метрик",
322
+ value=True,
323
+ info="Приводит текст к нижнему регистру, схлопывает пробелы и убирает пунктуацию.",
324
+ )
325
+
326
+ run_button = gr.Button("Транскрибировать и посчитать метрики", variant="primary")
327
+
328
+ results_table = gr.Dataframe(
329
+ headers=["Модель", "WER", "CER", "Время (сек)"],
330
+ datatype=["str", "str", "str", "number"],
331
+ label="Результаты сравнения",
332
+ )
333
+ status_output = gr.Markdown("Статус появится после запуска.")
334
+
335
+ with gr.Row():
336
+ whisper_output = gr.Textbox(
337
+ label="Транскрипт: Sh1man Whisper Large V3 CT",
338
+ lines=12,
339
+ )
340
+ gigaam_output = gr.Textbox(
341
+ label="Транскрипт: GigaAM v3 e2e RNNT",
342
+ lines=12,
343
+ )
344
+
345
+ run_button.click(
346
+ fn=benchmark_audio,
347
+ inputs=[
348
+ audio_input,
349
+ reference_input,
350
+ reference_file_input,
351
+ model_selector,
352
+ normalize_checkbox,
353
+ ],
354
+ outputs=[
355
+ results_table,
356
+ whisper_output,
357
+ gigaam_output,
358
+ status_output,
359
+ ],
360
+ )
361
+
362
+ gr.Markdown(
363
+ """
364
+ Первая инференс-сессия может идти заметно дольше из-за скачивания весов.
365
+
366
+ `Whisper` здесь настроен как `faster-whisper` на CTranslate2 через `BatchedInferencePipeline`
367
+ с VAD по умолчанию и `beam_size=5`. `GigaAM` использует встроенный longform-режим через
368
+ `transcribe_longform` и VAD из `pyannote/segmentation-3.0`.
369
+ """
370
+ )
371
+
372
+
373
+ if __name__ == "__main__":
374
+ demo.queue(default_concurrency_limit=1).launch()
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.29.0
2
+ transformers==4.57.1
3
+ torch==2.8.0
4
+ torchaudio==2.8.0
5
+ jiwer>=3.0.5
6
+ faster-whisper>=1.1.0
7
+ sentencepiece>=0.2.0
8
+ hydra-core>=1.3.2
9
+ omegaconf>=2.3.0
10
+ accelerate>=1.7.0
11
+ pyannote.audio==4.0.0
12
+ torchcodec==0.7.0