Если у вас имеется собственный контактный центр, задача найти упоминание чего-либо конкретного в большом количестве аудиозаписей возникает регулярно. Недавно я опубликовал статью о том, как настраивать это решение с нуля. Во второй части я хочу показать, какие решения мне пришлось разработать дополнительно для использования речевой аналитики Яндекс SpeechSense, какие дополнительные задачи при этом появились и как их решать.
Задача, которую я решал, формулировалась вот так. Необходимо проанализировать 25000 аудиозаписей разговоров оператора с клиентом по телефону, найти и вывести список всех аудиозаписей, где есть поздравления с праздниками.
Задача 1
Необходимо перекодировать все аудиозаписи в формат wave. В документации вы найдете, что можете использовать 4 аудиоформата, но пример в инструкции только для формата wave. У вас 2 варианта, первый — выучить наизусть весь git репозиторий с API Яндекса и переписать пример из инструкции и второй — приводить все аудиозаписи всех разговоров в формат wave. Я выбрал второй вариант, кодек ffmpeg и простой скрипт recode.bat решил проблему. Берем любые аудиозаписи любого из указанных форматов и перекодируем их в .wav вот так:
@echo off set converter="c:/ffmpeg-4.4-essentials_build/bin/ffmpeg.exe" set outfolder="wave" mkdir %outfolder% for %%f in (*.wma,*.mp3,*.mp4,*.ogg,*.wav,*.m4a,*.aac) do %converter% -i "%%f" %outfolder%/"%%~nf.wav"
Скрипт берет все аудиофайлы из папки, в которой лежит и в папку /wave/ складывает перекодированные в формат wavе файлы с тем же именем и расширением .wav.
Задача 2
Для подгрузки аудиозаписи в SpeechSense желательно иметь файл с метаданными аудиозаписи. Инструкция предлагает 2 варианта, создать .json файл к каждому аудио самостоятельно, либо, при отсутствии файла, использовать всегда вот такой формат:
now = datetime.datetime.now().isoformat() metadata = { 'operator_name': 'Operator', 'operator_id': '1111', 'client_name': 'Client', 'client_id': '2222', 'date': str(now), 'date_from': '2023-09-13T17:30:00.000', 'date_to': '2023-09-13T17:31:00.000', 'direction_outgoing': 'true', }
В моем случае метаданные я мог получить только из имени файла, и было два способа, которыми эти имена формировались.
В первом случае в имени файла был только ID звонка.
import os import json from datetime import datetime # Папка с файлами в формате wave из предыдущего скрипта. wave_folder = 'c:\\speech_sense\\upload_data\\calls\\wave\\' # Получение текущей даты в формате YYYY-MM-DDTHH:MM:SS.SSS. # Яндекс требует все даты именно в таком формате, пришлось повозиться, чтобы # отформатировать время корректно. При любых отклонениях от этого формата # загрузка аудио в SpeechSense заканчивается ошибкой. current_date = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] # Проходим по всем файлам в папке for filename in os.listdir(wave_folder): if filename.endswith('.wav'): # Создание шаблона JSON для каждого файла data = { "operator_name": os.path.splitext(filename)[0], "operator_id": os.path.splitext(filename)[0], "client_name": "Client", "client_id": "1", "date": current_date, "direction_outgoing": "true", "language": "ru" } # Для имени и ID оператора используем ID из названия файла. # По этому ID можно будет узнать, в каком файле найдены поздравления. # Имя JSON файла json_filename = f"{os.path.splitext(filename)[0]}.json" # Запись данных в JSON файл. Обязательно указывать кодировку windows-1251 # без параметра или в UTF-8 SpeechSense преобразует все русские буквы в кракозябры. with open(os.path.join(wave_folder, json_filename), 'w', encoding='windows-1251') as json_file: json.dump(data, json_file, ensure_ascii=False, indent=4) print("JSON файлы успешно созданы.")
Во втором случае в имени файла была информация о дате и времени звонка, контактном телефоне клиента и ID звонка. Шаблон имени файла был такой «YYYYMMDD-HHMM-toXXXXXXXXXX-ID.wav». Решение выглядит вот так
import os import json from datetime import datetime # Папка с файлами wave_folder = 'c:\\speech_sense\\upload_data\\calls\\wave\\' for filename in os.listdir(wave_folder): if filename.endswith('.wav'): # разбиваем имя файла по разделителю "-" data = os.path.splitext(filename)[0] parts = data.split("-") # склеиваем первую и вторую часть имени, где остаются дата и время звонка. date_str = parts[0] + parts[1] # приводим к необходимому для SpeechSense формату даты date_object = datetime.strptime(date_str, "%Y%m%d%H%M") formatted_date = date_object.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] data = { "operator_name": "Operator", "operator_id": parts[3], "client_name": "Respondent", "client_id": parts[2], "date": formatted_date, "direction_outgoing": "true", "language": "ru" } # пишем JSON файл, не забыв указать кодировку. json_filename = f"{os.path.splitext(filename)[0]}.json" with open(os.path.join(wave_folder, json_filename), 'w', encoding='windows-1251') as json_file: json.dump(data, json_file, ensure_ascii=False, indent=4) print("JSON файлы успешно созданы.")
Я понимаю, что тут найдутся специалисты, которые справедливо упрекнут, что нарушен принцип DRY, код повторяется и при желании не сложно написать одну функцию, которая будет извлекать метаданные из имен файла любого формата. Да, всё верно, рефакторинг кода уже запланирован на будущее. А тут изначально и планировались и проект по документации должен хранить в именах файлов только ID. Реальность оказалась сложнее. Вторая часть задачи возникла совершенно внезапно, когда весь код уже был написан.
Задача 3
Внезапно оказалось, что грузить и анализировать все аудиозаписи полностью очень дорого, такого бюджета у проекта не было. Быстро прослушав какие-то части и прогнав несколько сотен через бесплатные или десктопные STT решения, я обнаружил, что поздравления встречаются только в конце аудиофайла. Появилась идея отрезать только последние 30 секунд и проанализировать их. Установив в python модуль ffmpeg и прописав в windows PATH путь к кодеку удалось решить задачу вот так:
import os import ffmpeg from pydub import AudioSegment mp3_folder = 'c:\\speech_sense\\upload_data\\calls\\' wave_folder = 'c:\\speech_sense\\upload_data\\calls\\wave\\' finish_fragment_folder = 'c:\\speech_sense\\upload_data\\calls\\wave\\finish_fragment\\' def convert_mp3_to_wav(mp3_path, wav_path): """ Конвертируем MP3 в WAV используя модуль и установленный кодек ffmpeg. """ try: ffmpeg.input(mp3_path).output(wav_path).run() print(f"Converted {mp3_path} to {wav_path}") except Exception as e: print(f"Error converting {mp3_path} to WAV: {e}") def extract_last_30_seconds(wav_path, output_path): """ Извлекаем последние 30 секунд в WAV файле. """ try: audio = AudioSegment.from_wav(wav_path) last_30_seconds = audio[-30000:] # оставляем последние 30000 мс (30 секунд) last_30_seconds.export(output_path, format="wav") print(f"Extracted last 30 seconds to {output_path}") except Exception as e: print(f"Error extracting last 30 seconds from {wav_path}: {e}") if __name__ == "__main__": # перебираем все файлы и конвертируем. for filename in os.listdir(mp3_folder): if filename.endswith('.mp3'): mp3_file = os.path.join(mp3_folder, filename) wave_filename = f"{os.path.splitext(filename)[0]}.wav" wav_file = os.path.join(wave_folder, wave_filename) finish_fragment_filename = f"{os.path.splitext(filename)[0]}_last_30_seconds.wav" output_last_30_seconds = os.path.join(finish_fragment_folder, finish_fragment_filename) # Если Вам, уважаемый читатель, кажется, что решение Задачи 1 не подходит, # Просто сотрите символ комментария со стороки внизу. # Функция convert_mp3_to_wav() решает задачу 1 средствами python # convert_mp3_to_wav(mp3_file, wav_file) extract_last_30_seconds(wav_file, output_last_30_seconds)
На calls\wave\finish_fragment\ повторно пришлось прогнать скрипт из Задачи 2 и получить метаданные уже по обрезанным фрагментам.
Задача 4
Все аудиозаписи в проекте рассортированы по отдельным папкам, по дням для каждого подрядчика. Подрядчиков десятки, нужно собрать все MP3 файлы мне на компьютер, в папку calls.
Сразу признаюсь, что с автоматизацией этой задачи я не справился. Python молча и напрочь отказался создавать список директорий. Падал с ошибкой в строке 6. Причин такого поведения я обнаружить или нагуглить не смог. Подозреваю, что дело в пробелах, русских буквах в названиях директорий или в том, что файлы лежат на сетевых дисках.
Код решения я всё равно выложу, возможно кто-то увидит, объяснит и поправит ошибки. Буду благодарен.
import os import shutil import glob # список directories и переменную directories_list python отказывался создавать directories = ['/path/to/dir1', '/path/to/dir2', '/path/to/dir3'] directories_list = '/path/to/dir3' destination_dir = 'c:\\speech_sense\\upload_data\\calls\\' os.makedirs(destination_dir, exist_ok=True) def copy_mp3_files(directories, destination): for directory in directories: mp3_files = glob.glob(os.path.join(directory, '*.mp3')) for mp3 in mp3_files: shutil.copy(mp3, destination) copy_mp3_files(directories, destination_dir)
Задача 5
Начальные условия задачи были такие:
-
У меня есть 25000 готовых к загрузке аудиофайлов.
-
К каждому аудиофайлу созданы метаданные в .json с таким-же названием.
-
Примера с пакетной загрузкой в документации Яндекса нет.
-
Грузить файлы я умею только по одному.
-
Если прописать команду загрузки файла (см. первую часть статьи) в файл PowerShell .ps1 и запустить — ничего, кроме сообщения, что команду выполнить нельзя, не происходит.
Проблему с запуском файлов .ps1 я решил вот так: (https://ru.stackoverflow.com/questions/935212/powershell-выполнение-сценариев-отключено-в-этой-системе)
Открываем терминал с правами администратора
Пишем и запускаем: Set-ExecutionPolicy RemoteSigned
На вопрос отвечаем: A (Да для всех)
Далее я создал каждому аудиофайлу свой файл .ps1 для загрузки в SpeechSense.
import os wave_directory = 'c:\\speech_sense\\upload_data\\calls\\wave\\finish_fragment\\' output_directory = 'c:\\speech_sense\\upload_data\\calls\\powershell\\' # Constants for connection ID and key CONNECTION_ID = 'ID подключения' # Замените на свой KEY = 'API_KEY' # Замените на свой def generate_powershell_script(audio_filename): # Определяем имя файла для метаданных в .json взяв исходное имя файла без расширения base_name = os.path.splitext(audio_filename)[0] json_filename = f"{base_name}.json" # пишем команду PowerShell которая нужна для загрузки # обратите внимание, где стоят кавычки "". Без них .ps1 ничего не запускал cmd = f'py upload_grpc.py --audio-path "{wave_directory}\\{audio_filename}" --meta-path "{wave_directory}\\{json_filename}" --connection-id {CONNECTION_ID} --key {KEY}' # Имя скрипта PowerShell ps_script_filename = f"{base_name}.ps1" # Пишем команду в файл PowerShell with open(os.path.join(output_directory, ps_script_filename), 'w') as ps_script_file: ps_script_file.write(cmd) print(f"Generated {ps_script_filename}") def main(): # Проверяем, существует ли основная директория if not os.path.isdir(wave_directory): print(f"The directory {wave_directory} does not exist.") return # Формируем скрипт PowerShell для каждого файла .wav в директории for filename in os.listdir(wave_directory): if filename.endswith('.wav'): generate_powershell_script(filename) if __name__ == '__main__': main()
Задача 6
Запустить все файлы в папке оказалось легко средствами PowerShell. Как я уже упоминал ранее, запускаем его от имени администратора в папке, где лежит output от задачи 5. У меня это c:\speech_sense\upload_data\calls\powershell\
Set-ExecutionPolicy RemoteSigned Get-ChildItem -Path . -Filter "*.ps1" | ForEach-Object { powershell -File $_.FullName }
Все файлы подгрузились примерно за ночь. Стоимость обработки составила примерно рубль на один фрагмент. В проекте с аудиофайлами перед загрузкой необходимо создать тег, в котором ищем фразы поздравления с праздником. Обязательно перед загрузкой файлов. Новые и отредактированные теги на уже загруженные файлы не работают. Хотите новый тег создать и посмотреть, где он встречается — грузите и платите за расшифровку заново.
Установив фильтр на этот тег для высказываний оператора, получается список аудиозаписей и их ID, в которых есть поздравление с праздником.
Список ID для таких аудиозаписей можно скачать в CSV формате, По ID легко найти и поощрить операторов, которые при звонке клиента, проявляют инициативу и показывают уважение к клиенту.
ссылка на оригинал статьи https://habr.com/ru/articles/825162/
Добавить комментарий