Поставленная задача: распознавать речь по аудиозаписям диалогов сотрудников и клиентов.

Применяем Whisper.

Работаем в Colab.

Помимо распознавания речи данная модель Whisper имеет штатную функцию тайминга.

Исходный ресурс и документация здесь.

По документации:

Whisper — это универсальная модель распознавания речи. Она обучена на большом наборе данных разнообразного аудио, а также представляет собой многозадачную модель, которая может выполнять многоязычное распознавание речи, перевод речи и идентификацию языка.

Базовое применение по документации

Модель устанавливается стандартно.

Есть несколько модификаций (tiny, base, small, medium, large), в Colab помещаются все.

!pip install -U openai-whisper
import whisper

model_name = 'large'
model = whisper.load_model(model_name)

Базовое применение модели также простое и стандартное.

result = model.transcribe(file)
print(result["text"])

Мы же будем распознавать речь двух участников в двухканальном аудиофайле и добавим тайминг.

Загружаем аудиофайл

Загружаем файл с компьютера, запоминаем название, создаем папку для будущего архива.

import os
from google.colab import files

uploaded = files.upload()
file = next(iter(uploaded.keys()))
source_file_name = file.replace('.wav','')

path = "/content/" + source_file_name
os.mkdir(path)

Убеждаемся, что верно сохранили наименование файла и что в файле 2 канала.

print(source_file_name)

audio_file = wave.open(source_file_name + '.wav')
CHANNELS = audio_file.getnchannels()
print("Количество каналов:", CHANNELS)

Определяем язык

# load audio and pad/trim it to fit 30 seconds
audio = whisper.load_audio(source_file_name + '.wav')
audio = whisper.pad_or_trim(audio)

# make log-Mel spectrogram and move to the same device as the model
mel = whisper.log_mel_spectrogram(audio).to(model.device)

# detect the spoken language
_, probs = model.detect_language(mel)
print(f"Detected language: {max(probs, key=probs.get)}")

Необходимо обратить внимание, что определение языка осуществляется по первым 30 секундам. Это иногда вводит в заблуждение, так как собеседники могут поприветствовать друг друга на одном языке, а потом определить, что им удобнее разговаривать на другом языке и перейти на другой язык.

Создаем 2 файла по одной дорожке

Эта часть не относится непосредственно к Whisper и к распознаванию речи. Здесь читаем из файла все семплы, обнуляем каждый четный и создаем новый файл. Аналогично поступаем с нечетными семплами.

Код разбивки файла на 2 дорожки
import wave, struct

# файл делится на две дорожки и создается два файла

audio_file = wave.open(source_file_name + '.wav')

SAMPLE_WIDTH = audio_file.getsampwidth() # глубина звука
CHANNELS = audio_file.getnchannels() # количество каналов
FRAMERATE = audio_file.getframerate() # частота дискретизации
N_SAMPLES = audio_file.getnframes() # кол-во семплов на каждый канал

N_FRAMES = audio_file.getnframes()

# Определяем параметры аудиофайла

nchannels = CHANNELS
sampwidth = SAMPLE_WIDTH
framerate = FRAMERATE
nframes = N_FRAMES

comptype = "NONE"  # тип компрессии
compname = "not compressed"  # название компрессии

# узнаем кол-во семплов и каналов в источнике
N_SAMPLES = nframes
CHANNELS = nchannels

def create_file_one_channel(name):

  # создаем пустой файл в который мы будем записывать результат обработки в режиме wb (write binary)
  out_file = wave.open(name, "wb")

  # в "настройки" файла с результатом записываем те же параметры, что и у "исходника"
  out_file.setframerate(framerate)
  out_file.setsampwidth(sampwidth)
  out_file.setnchannels(CHANNELS)

  # обратно перегоняем список чисел в байт-строку
  audio_data = struct.pack(f"<{N_SAMPLES * CHANNELS}h", *values_copy)

  # записываем обработанные данные в файл с резхультатом
  out_file.writeframes(audio_data)

##########

print('started')

# читаем из файла все семплы
samples = audio_file.readframes(N_FRAMES)

# просим struct превратить строку из байт в список чисел
# < - обозначение порядка битов в байте (можно пока всегда писать так)
# По середине указывается общее количество чисел, это произведения кол-ва семплов в одном канале на кол-во каналоов
# h - обозначение того, что одно число занимает два байта

values = list(struct.unpack("<" + str(N_FRAMES * CHANNELS) + "h", samples))
print(values[:20])

values_copy = values[:]

# обнулим каждое четное значение
for index, i in enumerate(values_copy):
    if index % 2 == 0:
      values_copy[index] = 0

create_file_one_channel('1_channel.wav')
print(values_copy[:20])

values_copy = values[:]

# обнулим каждое нечетное значение
for index, i in enumerate(values_copy):
    if index % 2 != 0:
      values_copy[index] = 0

create_file_one_channel('2_channel.wav')
print(values_copy[:20])

Получили файл 1-го канала и файл 2-го канала: 1_channel.wav и 2_channel.wav.

Транскрибация 1 канала

Транскрибируем файл с 1 каналом

from datetime import timedelta

source_file_name_channel = '1_channel'
print(source_file_name_channel)

# load audio and pad/trim it to fit 30 seconds
audio = whisper.load_audio(source_file_name_channel + '.wav')
audio = whisper.pad_or_trim(audio)

# make log-Mel spectrogram and move to the same device as the model
mel = whisper.log_mel_spectrogram(audio).to(model.device)

# detect the spoken language
_, probs = model.detect_language(mel)
print(f"Detected language: {max(probs, key=probs.get)}")
print('started...')
print()

result = model.transcribe(source_file_name_channel + '.wav',)

segments = result['segments']

text_massive = []

for segment in segments:
    startTime = str(0)+str(timedelta(seconds=int(segment['start'])))
    endTime = str(0)+str(timedelta(seconds=int(segment['end'])))
    text = segment['text']
    segmentId = segment['id']+1
    segment = f"{segmentId}. {startTime} - {endTime}\n{text[1:] if text[0] == ' ' else text}"
    #print(segment)
    text_massive.append(segment)

print()
print('Finished')

и сохраняем результат в файл формата docx.

!pip install python-docx
from docx import Document

# сохраняем текст с таймингом

text = text_massive

# создаем новый документ
doc = Document()

# добавляем параграф с текстом
doc.add_paragraph(source_file_name + '_' + source_file_name_channel + '_' + model_name)
for key in text:
  doc.add_paragraph(key)

# сохраняем документ
doc.save(path + '/' + source_file_name + '_' + source_file_name_channel + '_text_timing_' + model_name + '.docx')

Со вторым каналом все то же самое, только вместо "1_channel" применяем "2_channel".
Также возможно применить цикл из этих двух значений, не принципиально.

На данном этапе получили 2 текстовых файла с текстом и таймингом, и эти файла лежат в созданной ранее папке с соответствующим названием. Осталось создать архив и скачать.

Создание и скачивание архива

import shutil/

# Создание архива с файлами из папки
shutil.make_archive(path + '_' + model_name, 'zip', path)

# Скачивание архива
files.download(path + '_' + model_name + '.zip')

Удаление файлов и папок

Если нужно обработать "вручную" еще файл, то следующий код удалит из Colab применяемые и полученные ранее файлы и папки.

# задаем сразу все и сразу все удаляем

file_massive = [
    source_file_name + '.wav',
    source_file_name + '_' + model_name + '.zip',
    '1_channel.wav',
    '2_channel.wav',
    path + '/' + source_file_name + '_1_channel_text_timing_' + model_name + '.docx',
    path + '/' + source_file_name + '_2_channel_text_timing_' + model_name + '.docx'
  ]

# Удаление файла
for key in file_massive:
  print(key)
  os.remove(key)
print()

# Удаление папки
print(path)
os.rmdir(path)
print()

print('Finished')

Colab очищен и можно работать с другим файлом.

Дополнение

Если файлов несколько и хочется автоматизировать, то возможно закачать все аудиофайлы и запустить все фрагменты подряд в едином цикле.

Совсем для автоматизации возможно скачивать файлы из карточки клиента или из специального хранилища и после транскрибации сохранять диалог обратно в карточку клиента или в специальное хранилище.

Транскрибация сразу на английский

В Whisper есть интересная возможность - переводить сразу на английский, минуя текстовый вывод. Это может быть полезно, если файлы на разных языках и их нужно анализировать единым способом. В этом случае целесообразно приводить все диалоги к английскому и в дальнейшем обрабатывать уже на английском.

Для транскрибации сразу на английский применяется дополнительное указание.

result = model.transcribe(source_file_name_channel + '.wav', task="translate")

Примечание

Если Вы обнаружили в статье неточность, или считаете полезным что-либо добавить - пожалуйста, сообщите в комментариях.

Комментарии (3)


  1. NickyX3
    20.10.2023 08:38
    +1

    Для Whisper есть нормальные CLI & GUI бинарники (по крайней мере для Win). Работает нормально так. Следует еще заметить, что в случае диалога - он начинает расставлять тире в начале строчек (показывая что это таки диалог) не сразу, потупит от 30 секунд до пары минут записи. Проверено на паре десятков интервью, которые мы тут транскрибировали с диктофона


  1. vsviridov
    20.10.2023 08:38
    +2

    Когда пытался им распознавать диалоги с двух дорожек (одна дорожка на человека) он начинал осень сильно галлюцинировать, т.к. были очень длинные паузы. Пришлось микшировать и распознать одним файлом.

    С диалогами у меня после где-то получаса пропадают знаки препинания и все становится с маленькой буквы.

    Полуторачасовая запись распознаётся в плане текста хорошо, но обычно проблемы со знаками препинания, репликами в диалогах, и он очень интересно расставляет тайминги если использовать сохранение в формат субтитров SRT. Типа последнее слово в предложении будет в новом титре. Приходится потом в ДаВинчи Резлов руками двигать...

    Но в целом все равно быстрее чем транскрибировать полтора часа аудиозаписи вручную.

    Не знаю, может раскошелюсь на полную версию Резолва, там вроде добавили распознание голоса нативно...


  1. aborouhin
    20.10.2023 08:38
    +1

    Whisper прекрасно справляется с записями телефонных разговоров. Но вот запись судебного заседания, сделанная с лежащего на столе телефона (т.е. шум переворачиваемых рядом с ним бумажек иногда сильно громче, чем голос с другой стороны зала) - вообще никак. При этом сберовский SaluteSpeech с той же записью справляется удовлетворительно (насколько это вообще возможно).