2025, Oct 30 20:48

Сбой ALSpeechRecognition на Pepper: конфликт modifiable_grammar и как его устранить

Как устранить ошибку 'A grammar named modifiable_grammar already exists' в ALSpeechRecognition на Pepper: безопасная смена языка, правильная загрузка словаря

При создании голосовых сценариев для Pepper на NAOqi одну, казалось бы, простую операцию легко превратить в источник сбоев — установка нового словаря в ALSpeechRecognition. При повторных запусках некоторые разработчики сталкиваются с аварийным завершением с сообщением: A grammar named "modifiable_grammar" already exists. Ниже — краткое разъяснение, что это провоцирует, как воспроизвести проблему и какой практический прием помогает ее надежно устранять на практике.

Воспроизводим проблему на минимальном сценарии взаимодействия

Следующий пример запускает цикл взаимодействия Pepper на Python 2.7. Он настраивает ALSpeechRecognition, динамически формирует словарь и подписывается на событие, чтобы получить ответ пользователя. Имена условные, но поведение полностью соответствует типичной конфигурации, на которой возникает ошибка.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
from naoqi import ALProxy
import time
LEXICON_READY_KEY = "MyApp/VocabInitialized"
ALT_LEXICONS = []
class PepperDialogAgent:
    def __init__(self, host="localhost", port=9559):
        self.host = host
        self.port = port
        self.tts_srv = None
        self.asr_srv = None
        self.mem_srv = None
        self._connect_services()
    def _connect_services(self):
        try:
            self.tts_srv = ALProxy("ALTextToSpeech", self.host, self.port)
            self.asr_srv = ALProxy("ALSpeechRecognition", self.host, self.port)
            self.mem_srv = ALProxy("ALMemory", self.host, self.port)
            self.asr_srv.setLanguage("Spanish")
            print("Robot services ready")
        except Exception as e:
            print("Failed to connect: {}".format(e))
            print("Make sure the robot is on, IP is correct, and you are on the same network.")
            raise
    def prompt_user(self, prompt_text, choices_map, wait_limit=15, conf_min=0.5):
        if not self.tts_srv or not self.asr_srv or not self.mem_srv:
            print("Initialization error")
            return (None, None)
        try:
            candidate_terms = list(choices_map.keys())
            self._configure_lexicon(candidate_terms)
            self.tts_srv.say(prompt_text)
            self.asr_srv.subscribe("DynASRSession")
            print("Listening...")
            print("Expected choices: {}".format(", ".join(candidate_terms)))
            self.mem_srv.insertData("WordRecognized", [])
            detected = self._await_input(wait_limit, conf_min)
            if detected:
                return self._map_result(detected, choices_map)
            else:
                self.tts_srv.say("No pude escuchar tu respuesta. Intenta hablar más claro.")
                return (None, None)
        except Exception as e:
            print("Interaction error: {}".format(e))
            return (None, None)
        finally:
            try:
                self.asr_srv.unsubscribe("DynASRSession")
            except:
                pass
    def _configure_lexicon(self, responses_list):
        try:
            self.asr_srv.pause(True)
            terms = []
            for r in responses_list:
                terms.append(r.lower())
                terms.append(r.upper())
                terms.append(r.capitalize())
            terms = list(set(terms))
            self.asr_srv.setVocabulary(terms, False)
            self.asr_srv.pause(False)
            print("Vocabulary loaded: {}".format(terms))
        except Exception as e:
            print("Vocabulary setup failed")
            print(e)
            raise e
    def _await_input(self, wait_limit, conf_min):
        picked = False
        t0 = time.time()
        time.sleep(1.0)
        while not picked and (time.time() - t0) < wait_limit:
            wr = self.mem_srv.getData("WordRecognized")
            if wr and len(wr) > 1:
                heard = wr[0]
                conf = wr[1]
                print("Heard '{}' with confidence {:.2f}".format(heard, conf))
                if conf > conf_min:
                    print("Accepted: {}".format(heard))
                    self.mem_srv.insertData("WordRecognized", [])
                    return heard
                else:
                    print("Confidence too low ({:.2f}), continuing...".format(conf))
                    self.mem_srv.insertData("WordRecognized", [])
            time.sleep(0.1)
        return None
    def _map_result(self, heard_word, choices_map):
        lowered = heard_word.lower()
        for key, payload in choices_map.items():
            if lowered == key.lower():
                msg = payload.get("text", "")
                val = payload.get("value", 0)
                print("User said '{}' - returning: ('{}', {})".format(heard_word, msg, val))
                return (msg, val)
        print("No mapping for: '{}'".format(heard_word))
        return (None, None)
def prompt_pepper_user(prompt_text, choices_map, host="localhost", port=9559, wait_limit=15):
    agent = PepperDialogAgent(host, port)
    return agent.prompt_user(prompt_text, choices_map, wait_limit)
if __name__ == "__main__":
    print("\n=== Example: Multiple options ===")
    q2 = "¿Qué te gustaría hacer? Puedes decir: bailar, cantar o hablar."
    options2 = {
        "bailar": {"text": "¡Perfecto! Vamos a bailar juntos.", "value": 1},
        "cantar": {"text": "¡Qué divertido! Me encanta cantar.", "value": 2},
        "hablar": {"text": "Excelente, podemos tener una buena conversación.", "value": 3}
    }
    txt, val = prompt_pepper_user(q2, options2)
    print("Result: ('{}', {})".format(txt, val))
    if val == 1:
        print("Activating dance mode...")
    elif val == 2:
        print("Activating singing mode...")
    elif val == 3:
        print("Activating conversation mode...")
    else:
        print("No answer recognized")
    print("\n=== Example: Yes/No ===")
    q1 = "¿Tienes alguna otra pregunta?"
    options1 = {
        "si": {"text": "¡Vamos!", "value": 1},
        "no": {"text": "Entiendo. Ha sido un placer. No dudes en volver a consultarme.", "value": 0}
    }
    txt, val = prompt_pepper_user(q1, options1)
    print("Result: ('{}', {})".format(txt, val))
    if txt:
        helper = PepperDialogAgent()
        helper.tts_srv.say(txt)

Что происходит под капотом

Сбой формулируется прямо: стек распознавания выдает A grammar named "modifiable_grammar" already exists. На практике это чаще всего проявляется при втором запуске взаимодействия — как раз в момент, когда загружается новый словарь. Платформа сигнализирует, что ресурс грамматики с таким именем уже существует, и попытка добавить еще один с тем же идентификатором завершается неудачей. Есть и важное ограничение в официальной документации API, которое легко упустить: setLanguage нельзя вызывать одновременно с другими методами ALSpeechRecognition или ALDialog. Учитывая это, становятся понятны две вещи: смена языка должна выполняться отдельно от любой активной сессии распознавания, а сброс языкового контекста помогает ALSpeechRecognition повторно инициализировать внутреннее состояние грамматик.

Практическое решение: краткая смена языков, затем установка целевого языка и словаря

Надежный способ избавиться от конфликта modifiable_grammar — принудительно выполнить «чистую» переинициализацию языка перед установкой нового словаря и запуском распознавания. Шаблон прост: на короткое время переключитесь между установленными языками, затем задайте нужный язык, после чего загрузите словарь и подпишитесь на распознавание. Пример ниже следует именно такому порядку и выполняет все вызовы строго до начала распознавания.

def init_asr_pipeline(asr_proxy, target_language, keyword_list):
    asr_proxy.setLanguage("German")
    asr_proxy.setLanguage("English")
    asr_proxy.setLanguage(target_language)
    asr_proxy.setVocabulary(keyword_list, False)
    asr_proxy.setAudioExpression(True)
    asr_proxy.setVisualExpression(True)
    asr_proxy.pause(False)
    asr_proxy.subscribe("speech_recognition")

Применительно к приведенному выше сценарию ключевой момент — выполнить «переключение» языка и финальный setLanguage непосредственно перед загрузкой словаря и подпиской. Если вы ставите распознавание на паузу при изменении словаря, сохраните этот порядок: пауза, изменение словаря, возобновление — и только затем подписка. Так вы соблюдете рекомендацию документации не смешивать setLanguage с другими вызовами речевого стека одновременно.

Почему это важно

Голосовые сценарии часто запускаются многократно в рамках одной сессии, особенно при итерационном тестировании или в демонстрационных циклах. Если стек распознавания удерживает ранее созданный объект грамматики, а вы без оглядки вызываете setVocabulary, велика вероятность вновь получить тот же конфликт — и не всегда воспроизводимо. Явная переинициализация языкового контекста дает предсказуемую точку сброса и стабилизирует повторные запуски. Заодно это сокращает время на поиски «случайных» сбоев, которые на деле оказываются защитой движка от перезаписи существующего ресурса грамматики.

Итоги

Если при вызове ALSpeechRecognition.setVocabulary вы видите A grammar named "modifiable_grammar" already exists, пересоберите порядок инициализации. Убедитесь, что в момент изменения языка и словаря распознавание не активировано; выполните краткую смену языка для обновления контекста; затем задайте целевой язык, загрузите ключевые слова и подпишитесь. Такой порядок обычно обеспечивает стабильную работу распознавания речи Pepper при многократных запусках — без вмешательства в остальную логику взаимодействия.

Статья основана на вопросе на StackOverflow от Manuel и ответе DrR955.