2025, Sep 23 17:16
pyttsx3 и runAndWait: как озвучить несколько фраз подряд без сбоев
Почему в pyttsx3 после повторных вызовов runAndWait звучит только первая фраза. Два решения: переинициализация движка или постановка всех реплик в очередь.
Когда вы используете pyttsx3 для озвучивания нескольких фраз подряд, повторные вызовы runAndWait могут привести к тому, что будет произнесена только первая реплика. Это проявляется в простых сценариях — например, в игре на правописание: вы зачитаете слово, подождёте пользователя, затем озвучите следующее. Вторая фраза так и не воспроизводится, хотя код продолжает выполняться.
Минимальный пример
Следующий фрагмент запрашивает две реплики подряд с отдельными вызовами runAndWait и останавливает движок после каждого запуска. В итоге звучит только первая фраза.
import pyttsx3
vocal = pyttsx3.init()
vocal.say("Hello World")
vocal.runAndWait()
vocal.stop()
vocal.say("Hi")
vocal.runAndWait()
vocal.stop()
Что происходит на самом деле
Управляющий поток продолжается как обычно; программа не зависает. Если добавить печать маркеров, видно, что выполняются оба блока:
import pyttsx3
speaker = pyttsx3.init()
speaker.say("Hello World")
speaker.runAndWait()
print("HELLO CALLED")
speaker.stop()
speaker.say("Hi")
speaker.runAndWait()
print("HI CALLED")
speaker.stop()
Оба маркера выводятся, что указывает на состояние движка, а не на ваш управляющий код Python. Наблюдаемое поведение намекает на то, что текст при втором проходе не ставится в очередь или не обрабатывается как ожидается. Есть сообщения, что остановка очищает очередь событий, а не выполняет полноценное завершение, и существует сопутствующий отчёт под названием «Воспроизводятся только первые два сообщения в очереди». Поведение зависит от окружения: в одних конфигурациях всё работает нормально, в других — нет. В указанной конфигурации Windows 11 с движком по умолчанию вторая фраза не озвучивается.
Практические решения
Есть два надёжных способа продолжать работу без изменения общей логики программы.
Подход 1: Переинициализировать движок для каждой реплики
Создавайте свежий экземпляр движка каждый раз, когда нужно что-то произнести, затем запускайте и освобождайте его. Так вы изолируете состояние между вызовами и избегаете проблемной повторной эксплуатации.
import pyttsx3
def speak_once(msg):
    tts = pyttsx3.init()
    tts.say(msg)
    tts.runAndWait()
    del tts
speak_once("Hello World")
print("SAYING HELLO WORLD")
speak_once("Hi")
print("SAYING HI")
Если вам удобнее откладывать runAndWait и всё же сбрасывать движок перед каждой постановкой в очередь, возвращайте новый экземпляр и управляйте им снаружи:
import pyttsx3
voice_ref = ""
def queue_with_reset(text):
    global voice_ref
    del voice_ref
    voice_ref = pyttsx3.init()
    voice_ref.say(text)
    return voice_ref
runner = queue_with_reset("Hello World")
runner.runAndWait()
print("SAYING HELLO WORLD")
runner = queue_with_reset("Hi")
runner.runAndWait()
print("SAYING HI")
Если иногда нужно озвучить несколько фраз одним пакетом, выполняйте сброс только при необходимости и переиспользуйте тот же экземпляр для накопления элементов:
import pyttsx3
engine_ref = None
def enqueue(text, reset=True):
    global engine_ref
    if reset:
        engine_ref = pyttsx3.init()
        engine_ref.say(text)
        return engine_ref
    engine_ref.say(text)
    return engine_ref
runner = enqueue("Hello World")
runner.runAndWait()
print("SAYING HELLO WORLD")
runner = enqueue("Hi")
runner.runAndWait()
print("SAYING HI")
runner = enqueue("Hi")
runner = enqueue("Hello", reset=False)
runner.runAndWait()
Подход 2: Сначала поставить все фразы в очередь, затем запустить один раз
Пропустите промежуточные вызовы runAndWait. Поставьте в очередь всё, что нужно произнести, выполните один запуск и затем остановите движок.
import pyttsx3
narrator = pyttsx3.init()
narrator.say("Hello World")
narrator.say("Hi")
narrator.runAndWait()
narrator.stop()
Почему это важно
В интерактивных приложениях, например в упражнениях на правописание или аудирование, критична предсказуемая последовательность: произнести, дождаться ввода, снова произнести. Если движок некорректно обрабатывает вторую постановку в очередь после остановки или между запусками, пользовательский опыт ломается. Понимание того, как изолировать состояние движка или группировать реплики, даёт детерминированное поведение между сеансами и помогает избежать «тихих» сбоев, которые сложно отладить.
Выводы
Когда runAndWait требуется вызывать многократно, повторное использование одного и того же экземпляра движка на некоторых системах приводит к тому, что последующие фразы не воспроизводятся. Выхода два: переинициализировать движок для каждой реплики или ставить фразы в очередь и запускать один раз. Если вы полагаетесь на повторяющиеся подсказки, отдайте предпочтение инициализации при каждом вызове; если создаёте непрерывную речь — сначала поставьте всё в очередь и выполните один прогон. Если в вашем окружении поведение отличается, учтите, что результат зависит от ОС и версии библиотеки; известны сообщения о проблемах с обработкой очереди. Тестируйте на целевой платформе и держите жизненный цикл движка простым, чтобы избежать сюрпризов.
Материал основан на вопросе на StackOverflow от atthegreatestworld и ответе пользователя Aadvik.