"""Реестр наборов: лицензия, срез, обязательства при публикации. Один источник правды для всех скриптов бенчмарка. Лицензии разобраны по первоисточникам приведён на странице замера — здесь только то, что влияет на код: можно ли брать набор, в какой срез он идёт и что нельзя публиковать. Срезы разделены не для красоты. WER зависит от материала сильнее, чем от системы: чистая начитка в студию и спонтанная речь в шумной комнате дают разброс в разы. Одна общая цифра по смеси наборов не значит ничего и первым же вопросом будет «на чём мерили». """ from __future__ import annotations from dataclasses import dataclass # Срезы. Публикуем метрики по каждому отдельно. SLICE_CLEAN = 'clean_read' # подготовленная начитка, студийная или близкая к ней SLICE_SPONTANEOUS = 'spontaneous' # живая спонтанная речь, бытовой шум SLICE_TELEPHONY = 'telephony' # 8 кГц, узкая полоса @dataclass(frozen=True) class Part: """Откуда физически берётся тестовая часть. `hf` — репозиторий Hugging Face, из которого забирается ТОЛЬКО test-parquet через `api/datasets//parquet`: тянуть train ради теста бессмысленно, это гигабайты впустую. `manual` — путь, который командой не автоматизируется. """ hf: str | None = None config: str = 'default' split: str = 'test' text_field: str = 'text' # как называется колонка транскрипта в parquet audio_field: str = 'audio' rows: int = 0 # сколько записей ожидаем — сверка после загрузки manual: str = '' # инструкция человеку, если автоматически нельзя @dataclass(frozen=True) class Source: id: str title: str license: str slice: str hours: float # заявленный объём части, которую берём attribution: str # обязательная строка атрибуции при публикации parts: tuple[Part, ...] = () share_alike: bool = False # ShareAlike: пофайловый производный материал не публикуем note: str = '' SOURCES: tuple[Source, ...] = ( Source( id='common_voice_25_ru', title='Common Voice ru Scripted 25.0', license='CC0-1.0', slice=SLICE_CLEAN, hours=251.9, attribution='Mozilla Common Voice, CC0', parts=(Part(manual=( 'С октября 2025 Common Voice снят с Hugging Face: репозитории ' 'mozilla-foundation/common_voice_* — пустые заглушки. Единственный путь — ' 'Mozilla Data Collective, датасет cmn2h1dg201gro107lpynbbd6: ' 'https://mozilladatacollective.com/datasets/cmn2h1dg201gro107lpynbbd6 — ' 'нужен аккаунт и согласие с условиями в веб-интерфейсе, без него API даёт 401. ' 'Скачивается корпус целиком (~6,6 ГБ, ru: 201 947 клипов), тест выбирается ' 'по test.tsv (10 283 записи) после распаковки. Разложить как ' 'bench/data/common_voice_25_ru/{clips,test.tsv} и позвать fetch.py --from-local.' )),), note='Обязательств нет вовсе — основа замера. Эталоны без пунктуации и без цифр.', ), Source( id='ruls', title='Russian LibriSpeech (RuLS)', license='CC-BY-4.0', slice=SLICE_CLEAN, hours=98.0, attribution='Russian LibriSpeech, CC BY 4.0', parts=(Part(hf='istupakov/russian_librispeech', text_field='text_no_preprocessing', rows=1352),), note=( 'Начитка книг, атрибуция обязательна. Берём test-часть из репака. ОСТОРОЖНО ' 'с колонками: названия перевёрнуты относительно ожидаемого — text уже ' 'нормализован (нижний регистр, без пунктуации), а оригинал с пунктуацией и ' 'заглавными лежит в text_no_preprocessing. Берём оригинал: нормализацию ' 'делаем своим нормализатором симметрично с гипотезой, а пунктуацию иначе ' 'не измерить вовсе. Оригинал OpenSLR SLR96 — один архив 8,5 ГБ на train и ' 'test вместе, ради 1352 записей теста его качать незачем.' ), ), Source( id='golos_test', title='Golos test (SberDevices)', license='переработка CC-BY-SA-4.0 под право РФ', slice=SLICE_SPONTANEOUS, hours=12.6, attribution='Golos dataset, SberDevices', parts=( Part(hf='bond005/sberdevices_golos_10h_crowd', text_field='transcription', rows=9994), Part(hf='bond005/sberdevices_golos_100h_farfield', text_field='transcription', rows=1916), ), share_alike=True, note=( 'NonCommercial в лицензии НЕТ — брать можно. Обязательства: атрибуция и ' 'ShareAlike на производный материал. Публикуем агрегаты и скрипты, но не ' 'пофайловые эталоны и гипотезы, и не дообучаем на нём модели. Две части ' 'вместе дают ровно официальный тест: crowd 11,2 ч и farfield 1,4 ч, ' '11 910 файлов, 16 кГц. Оригинальные sc.link-архивы из README недоступны ' 'из текущей сети (сетевой фильтр), зеркало OpenSLR SLR114 — 17 ГБ ' 'train и test одним архивом.' ), ), Source( id='sova_rudevices', title='SOVA Dataset — RuDevices', license='CC-BY-4.0', slice=SLICE_SPONTANEOUS, hours=101.0, attribution='SOVA Dataset (RuDevices), CC BY 4.0', parts=(Part(hf='bond005/sova_rudevices', text_field='transcription', rows=5799),), note=( 'Берём только RuDevices — ручная разметка. RuYoutube (17 451 ч) размечен ' 'машинным распознаванием: как эталон для WER негоден методологически. Своего ' 'train/test деления SOVA не публикует, деление берём из репака и это указываем ' 'в отчёте. Полный корпус при необходимости — RuDevices.tar (10,4 ГБ) через ' 'публичный API Яндекс.Диска, public_key https://disk.yandex.ru/d/jz3k7pnzTpnTgw.' ), ), ) # Наборы, отвергнутые на этапе разбора лицензий. Держим списком в коде, чтобы никто # не добавил их «за компанию», не прочитав лицензию первоисточника. REJECTED: dict[str, str] = { 'open_stt': 'CC BY-NC — некоммерческая, сервис коммерческий', 'buriy': 'CC BY-NC — то же', 'balalaika2000h': 'CC BY-NC-ND — запрещены и коммерция, и производные', 'sova_ruyoutube': 'разметка машинная (ASR), эталоном для WER быть не может', } BY_ID: dict[str, Source] = {s.id: s for s in SOURCES} SHARE_ALIKE: set[str] = {s.id for s in SOURCES if s.share_alike} def attribution_block() -> str: """Блок атрибуций для страницы /benchmark и статьи. Требование лицензий, не вежливость: у CC BY и CC BY-SA это условие использования.""" return '\n'.join(f'- {s.title} — {s.attribution} ({s.license})' for s in SOURCES)