Что и почему

Во многих проектах, в которых я участвую, ASR — это крутая новинка. Как я уже писал ранее, ASR может повысить прозрачность процессов и может эффективно сочетаться с последующими приложениями для аналитики, поиска и моделирования.

Большинство вариантов использования, которые я видел, также имеют значительные финансовые последствия. Рассмотрим небольшую гипотетическую группу поддержки (10 человек), обрабатывающую скромные, равномерно распределенные 500 звонков в день со Средним временем обработки 10 минут, в течение которых мы бы ожидать около 5000 минут аудио в день. Для каждого из основных облачных провайдеров:

  • GCP. Если наша предполагаемая сумма вызовов составляет 100 000 минут в месяц, мы выбираем модель по умолчанию и используем звук, суммированный по каналам, включая 60 минут. бесплатной транскрипции, мы рассчитываем, что ежемесячная приблизительная стоимость составит 3500 австралийских долларов. Не включая вспомогательную инфраструктуру (облачное хранилище, удаленные экземпляры/конфигурации запуска).
  • AWS. Точно так же, если мы выбираем стандартные модели, пакетную обработку и регион AP в Сиднее, мы попадаем в группу AWS T1 (≤ 250 000 минут). Включая 60 минут бесплатного времени расшифровки, и мы рассчитываем, что ежемесячная приблизительная стоимость составит 3 600 австралийских долларов. Также не включая вспомогательную инфраструктуру.
  • Azure. Опять же, мы выбираем стандартную модель и учитываем щедрые 5 бесплатных часов транскрибирования, а ежемесячная приблизительная стоимость составляет 2400 австралийских долларов? strong> Это значительно дешевле, и я подозреваю, что есть некоторые ошибки округления часов, отражающие калькулятор цен Azure.

Учитывая эти финансовые последствия, я подумал, что было бы интересно изучить создание и оценку эквивалента с открытым исходным кодом услуг ASR облачного провайдера. Далее следует ASR для бедняков.

Сбор данных

Требования. В идеале у нас есть доступ к аудиозаписи, а также сопроводительная стенограмма, которую мы можем использовать для оценки любой модели/API ASR.

Youtube. Youtube кажется естественным ресурсом, достойным внимания, с доступом к обширным коллекциям аудио через видео, доступ к которым можно получить с помощью таких библиотек, как pytube. Существуют способы доступа к транскрипциям видео, которые сопровождают большинство видео на YouTube, но, как подробно описано здесь, эти транскрипции сами генерируются с помощью ASR. Не особенно идеально, если мы хотим оценить производительность модели ASR, что в идеале делается с использованием подтвержденной/сгенерированной человеком достоверной информации.

Подкасты Radio National. В конце концов я остановился на сборе подкастов Radio National. Важно отметить, что эти подкасты имеют:

  • Разнообразный контент. Это азбука! Научите меня тому, как отучиться от хронической боли и дополните ее отрывком о восстановлении дикой природы шотландского нагорья. С практической точки зрения широкий охват этого контента станет хорошей проверкой универсальности любого решения ASR.
  • Разные динамики и качество звука. Просматривая приведенные выше примеры^, мы видим, что в этих подкастах принимают участие от 3 до 5 гостей. Кроме того, быстрое прослушивание некоторых из этих подкастов показывает, что многие гости звонят на шоу. Кроме того, подкасты были сжаты для использования в Интернете в формате MP3. Это означает, что мы будем бороться с телефонным звуком, который был суммирован по громкоговорителям и преобразован в низкокачественный MP3. Это может стать настоящим испытанием. Кроме того, вот хороший подкаст, посвященный историческому дизайну телефонии Bell Labs и существенному компромиссу между качеством и размером, который приводит к тому, что большинство телефонных аппаратов звучат лоу-фай и потрескивают.
  • Разная продолжительность. Аналогичным образом продолжительность подкастов национальных радиостанций варьируется от 9 до 60 минут. Сама длина звука, как вход для модели ASR, почти наверняка станет еще одной проблемой.
  • Сопровождающая стенограмма. Все подкасты Radio National снабжены отличной сопроводительной стенограммой, расставленной пунктуацией и, по всей вероятности, проверенной специалистом по доступности из ABC. Качество гарантировано!

RN Scraping. Итак, Radio National. Если мы просматриваем соответствующую подстраницу стенограммы на основном веб-сайте RN, мы можем просмотреть ряд страниц стенограммы:

Используя красивый суп, мы можем собрать список URL-адресов страниц для конкретных эпизодов, перебирая ссылки на родительские страницы:

def get_podcast_page_urls(page_url, base_url):
    res = requests.get(page_url)
    soup = BeautifulSoup(res.content, "html.parser")
    podcast_page_urls = []
    for a in soup.find_all("a", href=True):
        if "/radionational/programs" in a["href"] and len(Path(a["href"]).parts) > 3:
            podcast_page_urls.append(f"{base_url}{a['href']}")
    return podcast_page_urls

Затем мы можем извлечь/загрузить URL-адрес файла MP3 и текст стенограммы для каждого эпизода, выполнив поиск по соответствующим тегам:

def get_podcast_mp3_link(page_soup):
    audio_elements = page_soup.find("audio")
    mp3_candidate_links = [e["src"] for e in audio_elements]
    if len(mp3_candidate_links) > 1:
        pod_scrape_logger.warning("More than 1 candidate mp3 URL found")
    else:
        return mp3_candidate_links[0]
def download_podcast_mp3(mp3_url, audio_dir, file_name):
    doc = requests.get(mp3_url)
    with open(audio_dir / f"{file_name}.mp3", "wb") as f:
        f.write(doc.content)
def get_podcast_transcript(page_soup):
    results = page_soup.find(id="transcript")
    return results.get_text(separator="\\n")

Постобработка. Одна вещь, которую я заметил, это то, что в стенограммах есть явные теги говорящего, а также несколько странных разных тегов новой строки и тегов оверлея (например, [sound intro], описывающий вступительную музыку к подкасту/ сегмент). Пример ниже:

Robyn Williams:
 Who got his PhD in forestry in Melbourne. Is he right? Well, here's a thought from the late James Lovelock:
James Lovelock:
 To me, clearance of the tropical forests is by far the most damaging thing that we are doing to the Earth and to people. You see, they are talked about in connection with the CO
2

Все это может создать проблемы для последующей оценки, где нас больше будут интересовать необработанные токены, в которых хранится их последовательность. Некоторая дополнительная постобработка выглядит следующим образом:

def remove_excess_char(
    input_string,
):
    # new lines
    text = re.sub("[\\n]{2,}", "\\n", input_string)
    # tabs
    text = re.sub("[\\t]{2,}", "\\t", text)
    # carriage returns
    text = re.sub("[\\r]{2,}", "\\r", text)
    # vertical tabs
    text = re.sub("[\\v]{2,}", "\\v", text)
    # n-repetitive spaces
    for n in range(2, 10)[::-1]:
        text = text.replace(" " * n, " ")
    return text
def remove_transcript_artefacts(transcript):
    filtered = []
    for line in transcript.replace("\\n:", ":\\n").split("\\n"):
        line = line.strip()
        # colon in initial fragment > speaker tag probably
        if ":" in line[:20]:
            line = line.split(":")[1].strip()
        # remove production audio overlay brackets/parens
        if "[" in line:
            line = re.sub("\\[(.*?)\\]", "", line)
        if "(" in line:
            line = re.sub("\\(.*?\\)", "", line)
        line = remove_excess_char(line)
        if len(line) == 0:
            continue
        if line.endswith(":"):
            # probably a speaker utterance mark
            continue
        filtered.append(line)
    return " ".join(filtered)

Кроме того, мы также хотим выполнить некоторые базовые проверки набора, чтобы убедиться, что каждый подкаст содержит аудио шоу, а также сопровождающий текст стенограммы:

def prune_pairless_transcripts(audio_output_dir, transcript_output_dir):
    # get intersection
    all_audio = set([e.stem for e in audio_output_dir.glob("./*.mp3")])
    all_transcript = set([e.stem for e in transcript_output_dir.glob("./*.txt")])
    intersecting_transcripts = all_audio.intersection(all_transcript)
    for file in audio_output_dir.glob("./*.mp3"):
        if file.stem not in intersecting_transcripts:
            pod_scrape_logger.warning(
                f"Could not find {file.name} in audio/transcript intersection; removing"
            )
            file.unlink()
    for file in transcript_output_dir.glob("./*.txt"):
        if file.stem not in intersecting_transcripts:
            pod_scrape_logger.warning(
                f"Could not find {file.name} in audio/transcript intersection; removing"
            )
            file.unlink()

И мы также хотели бы пока отфильтровать некоторые из больших подкастов, чтобы повысить скорость итерации нашего прототипа конвейера ASR. Для удобства это можно сделать, сгенерировав манифест набора данных; концепция, заимствованная из библиотеки NEMO Nvidia, которую мы увидим во второй части. В любом случае, манифесты полезны, потому что они позволяют нам манипулировать (запускать предварительные запросы > файловые действия) нашими расшифровками как ванильными фреймами данных:

def create_manifest(
    audio_output_dir, transcript_output_dir, podcast_min_len=5, podcast_max_len=15
):
    transcript_records = []
    for audio, transcript in zip(
        sorted(list(audio_output_dir.glob("./*.mp3"))),
        sorted(list(transcript_output_dir.glob("./*.txt"))),
    ):
        assert audio.stem == transcript.stem
        with open(transcript, "r") as f:
            transcript_text = f.read()
        transcript_records.append(
            {
                "transcript": transcript_text,
                "len_seconds": MP3(audio).info.length,
                "len_minutes": MP3(audio).info.length / 60,
                "audio_path": audio.resolve(),
                "transcript_path": transcript.resolve(),
            }
        )
    podcast_manifest = (
        pd.DataFrame(transcript_records)
        .assign(stem=lambda x: x.audio_path.apply(lambda y: y.stem))
        .assign(
            transcript_len=lambda x: x.transcript.apply(lambda y: len(y.split(" ")))
        )
        .query("len_minutes >= @podcast_min_len & len_minutes <= @podcast_max_len")
        .assign(
            wpm=lambda x: x.apply(lambda y: y.transcript_len / (y.len_minutes), axis=1)
        )
    )
    return podcast_manifest
def prune_transcripts_not_in_manifest(
    manifest, audio_output_dir, transcript_output_dir
):
    audio_file_names = [e.name for e in manifest.audio_path]
    transcript_file_names = [e.name for e in manifest.transcript_path]
    for file in audio_output_dir.glob("./*.mp3"):
        if file.name not in audio_file_names:
            pod_scrape_logger.warning(
                f"Could not find {file.name} in manifest; removing"
            )
            file.unlink()
    for file in transcript_output_dir.glob("./*.txt"):
        if file.name not in transcript_file_names:
            pod_scrape_logger.warning(
                f"Could not find {file.name} in manifest; removing"
            )
            file.unlink()

Ta da

Итак, подведем итоги: мы выбрали приложение/мотив для создания нашего собственного конвейера ASR, а также собрали небольшой набор данных транскриптов Radio National. Прохладный. Некоторые известные проблемы и будущие улучшения в вышеуказанной работе:

  • Оценка релевантности. Извечная проблема веб-скрейпинга заключается в том, что кодировать изменения во входных данных, как правило, сложно. Каждое изменение в исходном коде сайта может привести к поломке парсера, и есть большая вероятность, что, если вышеуказанные скрипты будут запущены снова через несколько месяцев, они будут сломаны и потребуют некоторой настройки.
  • Использование подкастов ABC. Подкасты национального радио подчиняются тем же некоммерческим условиям использования политики, что и другой контент ABC. Это означает, что стенограммы, по сути, подходят только для подобных сообщений в блогах без получения дополнительных разрешений.
  • Архивы Trove. Я заметил, что в trove есть списки СМИ RN, которые ссылаются на исходную страницу RN. Trove предоставляет явный поисковый интерфейс (гибкий внутри, я почти уверен) для использования, который позволит использовать более конкретные (периоды времени? типы контента? докладчики?) поиск подкастов RN, которые будут использоваться.

Вы можете найти весь код из части 1 здесь, которая также включает скрипт для загрузки подкастов/расшифровок ABC. Переходите на Часть 2!