Преобразование речи в текст с помощью ИИ на Python: пошаговый гайд

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

Проект моей группы представлял собой игру “речь-текст-речь”, и вот как она работает: пользователь выбирает персонажа для разговора, а затем устно высказывает ему все, что пожелает. Эти слова транскрибируются и отправляются в ChatGPT, который отвечает на них, как если бы он был персонажем. Затем ответ читается вслух с помощью технологии преобразования текста в речь.

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

Хотите увидеть код целиком? Вот ссылка!

Поток программы

После запуска сервера пользователь услышит, как приложение “заговорит”, предлагая ему выбрать персонажа, с которым он хочет поговорить, и начать беседу с выбранным персонажем. Каждый раз, когда пользователь собирается говорить вслух, он должен нажать и удерживать клавишу на клавиатуре во время разговора. После окончания разговора (и отпускания клавиши), запись будет транскрибирована с помощью Whisper (модели преобразования речи в текст от OpenAI), и отправлена в ChatGPT для получения ответа. Ответ будет зачитан вслух с помощью библиотеки преобразования текста в речь, и пользователь услышит его.

Реализация

Дисклеймер

Проект был разработан на операционной системе Windows и использует библиотеку pyttsx3, которая не совместима с чипами M1/M2. Поскольку pyttsx3 не поддерживается на Mac, пользователям рекомендуется изучить альтернативные библиотеки преобразования текста в речь, совместимые с macOS.

Интеграция с OpenAI

Я использовала две модели OpenAI: Whisper для перевода речи в текст и API ChatGPT для генерации ответов на основе ввода пользователем послания для своего собеседника. Хотя это стоит денег, принцип ценообразования приемлем, и лично мой счет за все время использования не превысил 1 доллара. Чтобы начать работу, я сделала первоначальный депозит в размере 5 долларов, и на сегодняшний день не исчерпала его, а срок действия этого депозита истекает только через год. За написание этой статьи я не получаю от OpenAI никакой оплаты или выгоды.

Как только вы станете владельцем API-ключа OpenAI, задайте его в качестве переменной среды, чтобы использовать при выполнении вызовов API. Не выкладывайте ключ в кодовую базу или любое другое публичное место, а также не делитесь им, если есть риск его перехвата.

Преобразование речи в текст: транскрибирование

Для реализации функции преобразования речи в текст была использована модель от OpenAI Whisper.

Ниже приведен фрагмент кода для функции, отвечающей за транскрибирование:

async def get_transcript(audio_file_path: str, 
text_to_draw_while_waiting: str) -> Optional[str]:
openai.api_key = os.environ.get("OPENAI_API_KEY")
audio_file = open(audio_file_path, "rb")
transcript = None

async def transcribe_audio() -> None:
nonlocal transcript
try:
response = openai.Audio.transcribe(
model="whisper-1", file=audio_file, language="en")
transcript = response.get("text")
except Exception as e:
print(e)

draw_thread = Thread(target=print_text_while_waiting_for_transcription(
text_to_draw_while_waiting))
draw_thread.start()

transcription_task = asyncio.create_task(transcribe_audio())
await transcription_task

if transcript is None:
print("Transcription not available within the specified timeout.")

return transcript

Эта функция помечена как асинхронная (async), поскольку вызов API может занять некоторое время, чтобы вернуть ответ. Мы ожидаем его, чтобы программа не выполнялась до тех пор, пока не будет получен ответ.

Как видите, функция get_transcript также вызывает функцию print_text_while_waiting_for_transcription. Почему? Поскольку получение транскрибирования занимает много времени, нам нужно информировать пользователя о том, что программа активно обрабатывает его запрос, а не застряла или не отреагировала. В результате текст постепенно выводится, пока пользователь ожидает следующего шага.

Сопоставление строк с помощью библиотеки FuzzyWuzzy для сравнения текстов

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

Для сравнения использовались следующие сценарии:

  • выбор персонажа из заранее определенного списка вариантов;
  • принятие решения о продолжении игры или отказе от нее;
  • при принятии решения о продолжении  —  выбор нового персонажа или сохранение текущего.

В таких случаях мы хотели сравнить результат транскрибирования ввода пользователя с вариантами из наших списков, поэтому решили использовать библиотеку FuzzyWuzzy для сопоставления строк.

Это позволило выбирать наиболее близкий вариант из списка, если результат сопоставления превышал заданный порог.

Вот фрагмент функции:

def detect_chosen_option_from_transcript(
transcript: str, options: List[str]) -> str:
best_match_score = 0
best_match = ""

for option in options:
score = fuzz.token_set_ratio(transcript.lower(), option.lower())
if score > best_match_score:
best_match_score = score
best_match = option

if best_match_score >= 70:
return best_match
else:
return ""

Получение ответа от ChatGPT

Имеющийся результат транскрибирования мы можем отправить в ChatGPT, чтобы получить ответ.

Для каждого запроса к ChatGPT мы добавили промпт с просьбой дать короткий и смешной ответ. Мы также указали ChatGPT, каким персонажем ему нужно притвориться.

Таким образом, функция выглядела следующим образом:

def get_gpt_response(transcript: str, chosen_figure: str) -> str:
system_instructions = get_system_instructions(chosen_figure)
try:
return make_openai_request(
system_instructions=system_instructions,
user_question=transcript).choices[0].message["content"]
except Exception as e:
logging.error(f"could not get ChatGPT response. error: {str(e)}")
raise e

А системные указания выглядели так:

def get_system_instructions(figure: str) -> str:
return f"You provide funny and short answers. You are: {figure}"

Преобразование текста в речь

Для преобразования текста в речь мы выбрали библиотеку Python под названием pyttsx3. Она не только проста в реализации, но и обладает рядом дополнительных преимуществ. Эта библиотека бесплатна, предоставляет два варианта голоса (мужской и женский) и позволяет выбрать скорость речи, которая измеряется в словах в минуту.

Когда пользователь начинает игру, он выбирает персонажа из заранее определенного списка вариантов. Если нам не удавалось найти подходящего персонажа в списке, мы случайным образом выбирали персонажа из списка “запасных”. В обоих списках каждый персонаж принадлежал к определенному полу, поэтому функция преобразования текста в речь также получала идентификатор голоса (voice ID), соответствующий выбранному полу.

Вот как выглядела функция преобразования текста в речь:

def text_to_speech(text: str, gender: str = Gender.FEMALE.value) -> None:
engine = pyttsx3.init()

engine.setProperty("rate", WORDS_PER_MINUTE_RATE)
voices = engine.getProperty("voices")
voice_id = voices[0].id if gender == "male" else voices[1].id
engine.setProperty("voice", voice_id)

engine.say(text)
engine.runAndWait()

Основной поток

Теперь, когда мы более или менее разобрались со всеми частями приложения, пришло время погрузиться в игровой процесс! Ниже показан основной поток. Вы можете заметить некоторые функции, которые мы не рассматривали (например, choose_figure и play_round). Загляните в репозиторий, чтобы изучить код целиком. В конечном счете большинство этих функций более высокого уровня связаны с внутренними функциями, которые мы разбирали выше.

Вот фрагмент основного игрового процесса:

import asyncio

from src.handle_transcript import text_to_speech
from src.main_flow_helpers import choose_figure, start, play_round, \
is_another_round


def farewell() -> None:
farewell_message = "It was great having you here, " \
"hope to see you again soon!"
print(f"\n{farewell_message}")
text_to_speech(farewell_message)


async def get_round_settings(figure: str) -> dict:
new_round_choice = await is_another_round()
if new_round_choice == "new figure":
return {"figure": "", "another_round": True}
elif new_round_choice == "no":
return {"figure": "", "another_round": False}
elif new_round_choice == "yes":
return {"figure": figure, "another_round": True}


async def main():
start()
another_round = True
figure = ""

while True:
if not figure:
figure = await choose_figure()

while another_round:
await play_round(chosen_figure=figure)
user_choices = await get_round_settings(figure)
figure, another_round = \
user_choices.get("figure"), user_choices.get("another_round")
if not figure:
break

if another_round is False:
farewell()
break


if __name__ == "__main__":
asyncio.run(main())

Нереализованные варианты

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

Сопоставление голоса ответа с “реальным” голосом выбранного персонажа

Представьте, что пользователь хочет поговорить со Шреком, Трампом или Опрой Уинфри. Мы хотели бы, чтобы библиотека или API для преобразования текста в речь озвучивали ответы голосом, соответствующим выбранному персонажу. Однако во время хакатона мы не смогли найти библиотеку или API, которые бы предлагали такую возможность по разумной цене.

Предоставление пользователю возможности поговорить с самим собой

Еще одна интригующая идея заключалась в том, чтобы предложить пользователям предоставить образец своей речи. Затем мы обучили бы модель по этому образцу и добились бы того, чтобы все ответы, сгенерированные ChatGPT, читались вслух голосом самого пользователя. В этом случае пользователь мог бы выбирать тон ответов (утвердительный, ободряющий, саркастический, сердитый и т. д.), но этот голос был бы очень похож на голос пользователя. Однако в рамках хакатона мы не смогли найти API, который бы поддерживал такую возможность.

Добавление фронтенда в приложение

Изначально мы планировали включить в приложение компонент фронтенда. Однако из-за того, что в последний момент количество участников команды изменилось, мы решили отдать предпочтение разработке бэкенда. В результате приложение в настоящее время работает на основе интерфейса командной строки (CLI) и не оснащено фронтендом.

Дополнительные улучшения, которые мы планируем ввести

Задержка  —  вот что беспокоит меня больше всего на данный момент.

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

Ссылка на репозиторий

Вот ссылка на проект целиком.

Выводы

В этой статье мы узнали, как создать игру с преобразованием речи в текст с помощью Python и соединить ее с искусственным интеллектом. Мы использовали модель Whisper от OpenAI для распознавания речи, поэкспериментировали с библиотекой FuzzyWuzzy для сопоставления текста, погрузились в магию разговорных способностей ChatGPT через API разработчика и воплотили все это в жизнь с помощью pyttsx3 для преобразования текста в речь. Хотя сервисы OpenAI (Whisper и ChatGPT для разработчиков) не бесплатны, они предоставляются за вполне разумную плату. 

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Naomi Kriger: Speech to Text to Speech with AI Using Python — a How-To Guide

Предыдущая статьяПолезные уроки из книги “Мышление, быстрое и медленное”
Следующая статьяSealed-интерфейс Kotlin: полное руководство для Android-разработчиков