Генерация текста на русском по шаблонам

    Когда я только начинал работать над своей текстовой игрой, решил, что одной из её главных фич должны стать красивые художественные описания действий героев. Отчасти хотел «сэкономить», поскольку в графику не умел. Экономии не получилось, зато получилась Python библиотека (github, pypi) для генерации текстов с учётом зависимости слов и их грамматических особенностей.

    Например, из шаблона:
    [Hero] [проходил|hero] мимо неприметного двора и вдруг [заметил|hero] играющих детей. Они бегали с деревянными мечами, посохами и масками чудовищ. Внезапно один из играющих остановился, выставил [игрушечный|hero.weapon|вн] [hero.weapon|вн], выкрикнул: «[Я|hero] [великий|hero] [Hero]! Получай!» — и бросился на «бестий». Они упали наземь, задрыгали руками-ногами, а после встали, сняли маски и засмеялись. [Хмыкнул|hero] и [сам|hero] [Hero], но не [стал|hero] выходить к малышне.
    Мы можем получить такой текст (жирным выделены изменяющиеся слова):
    Халлр проходил мимо неприметного двора и вдруг заметил играющих детей. Они бегали с деревянными мечами, посохами и масками чудовищ. Внезапно один из играющих остановился, выставил игрушечную золочёную шпагу, выкрикнул: «Я великий Халлр! Получай!» — и бросился на «бестий». Они упали наземь, задрыгали руками-ногами, а после встали, сняли маски и засмеялись. Хмыкнул и сам Халлр, но не стал выходить к малышне.
    Или такой:
    Фиевара проходила мимо неприметного двора и вдруг заметила играющих детей. Они бегали с деревянными мечами, посохами и масками чудовищ. Внезапно один из играющих остановился, выставил игрушечный катар, выкрикнул: «Я великая Фиевара! Получай!» — и бросился на «бестий». Они упали наземь, задрыгали руками-ногами, а после встали, сняли маски и засмеялись. Хмыкнула и сама Фиевара, но не стала выходить к малышне.

    Пара оговорок
    Оговорка 1. Я не лингвист и библиотека писалась «чтобы работала», а не «чтобы точно соответствовала всем правилам языка». Поэтому заранее извиняюсь за неточности в терминологии или неполную трактовку правил русского языка.

    Оговорка 2. Библиотека разрабатывалась около 5 лет назад, сейчас могли появиться (или дорасти до нормального состояния) альтернативные средства генерации текста. Например, что-нибудь интересное может быть в софте для локализации.

    О сложности генерации текстов


    Русский язык сложен во многих своих аспектах. В частности, слова имеют большое количество морфологических форм. Например, прилагательные могут иметь полную и краткую формы, изменяются по роду, числу, падежу, одушевлённости и степени сравнения. Выбор же конкретной формы зависит от других слов в предложении. Мы говорим «красивая женщина», но «красивый мужчина». Слово «красивый» в данном случае зависит от слов «мужчина» / «женщина» — его форма определяется родом главного слова.

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

    Я же хотел чего-то большего, чем простая зависимость от пола игрока, да ещё и чтобы пользователи сами могли добавлять новые тексты (а «средний» пользователь довольно безграмотен, как мы все знаем :-) ). Поэтому, не найдя подходящий софт, решил делать его самостоятельно.

    Возможности библиотеки


    UTG (universal text generator — не очень скромное название) позволяет создавать шаблоны для генерации текста с указанием:

    • переменных (например, имени персонажа);
    • зависимостей слов от переменных (например, прилагательного от существительного);
    • Зависимостей одних переменных от других;
    • Явных свойств слов и переменных (например, можно указать, что имя персонажа вставляется в родительском падеже);

    При формировании текста по шаблону:

    • На зависимые слова переносятся необходимые свойства главного слова. Например, на прилагательное переносится род существительного.
    • Согласуется форма зависимых слов с числительными (с учётом формы зависимых слов).
    • Модифицируются предлоги если необходимо (например, обо мне / о тебе), предлог для этого должен быть размечен.

    Дополнительно реализованы:

    • Словарь для хранения необходимых слов.
    • Хранилище шаблонов для их хранения по типу и выбора случайного.

    Библиотека «знает» о существовании существительных, прилагательных, местоимений, глаголов, причастий, чисел, предлогов и «цитат» (неизменяемого текста).

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

    Формат шаблонов и пример использования


    Давайте разберём простой шаблон:
    Вчера [mob] [укусил|mob] [hero|вн].
    В зависимости от значений переменных, шаблон может отобразиться как такой фразой:
    Вчера гиена укусила Халлра.
    так и такой:
    Вчера светлячки укусили привидение.
    Рассмотрим шаблон подробнее:

    • Вчера — обычный текст.
    • [mob] — переменная, вместо которой подставится название монстра.
    • [укусил|mob] — слово, зависимое от переменной, часть его свойств будет изменяться в зависимости от свойств названия монстра (например, число). Генератор текста автоматически распознаёт свойства формы слова и пытается их сохранить (например, будет распознано и сохранено прошедшее время, поэтому указывать его не надо).
    • [hero|вн] — переменная, вместо которой подставится имя героя. Дополнительно указано, что имя должно быть в винительном падеже.

    Больше примеров шаблонов
    Некоторые технические примеры можно найти в тестах.

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

    И переменные и зависимые слова в шаблоне выделяются одинаково и имеют следующий формат:

    • [ — открывающая квадратная скобка.
    • слово — зависимое слово или идентификатор переменной. Генератор сначала проверяет наличие переменной с таким именем, если такой переменной нет, то слово ищется в словаре.
    • | — вертикальная черта — разделитель, нужен если указываем дополнительные свойства.
    • имя переменной — переменная, от которой зависит форма слова, может отсутствовать.
    • | — вертикальная черта — разделитель, нужен если указываем дополнительные свойства.
    • свойства слова через запятую — описание требуемой формы слова (падеж, род и так далее). Их список можно найти на страницах проекта в github и pypi.
    • ] — закрывающая квадратная скобка.

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

    [переменная 1|переменная 2|вн,мр|переменная 3|прш,ед,од]

    В большинстве случаев хватает следующих форматов:

    • [переменная] — вставить переменную в нормальной форме (например, существительное в именительном падеже единственного числа).
    • [переменная|свойства] — вставить переменную с указанными свойствами.
    • [слово|переменная] — вставить слово, согласовав его с переменной (например, прилагательное «красивый» с существительным по роду и падежу).
    • [слово|переменная|свойства] — вставить слово, согласовав его с переменной и указав дополнительные свойства.

    Обратите внимание:

    • Указание свойств для слов и переменных действует только в месте вставки, поэтому, чтобы получить словосочетание «красивого героя» мы должны указать винительный падеж явно для двух слов: [красивый|hero|вн] [hero|вн].
    • Генератор текста умеет «угадывать» свойства слова по его форме, например, во фразе [hero] [побежал|hero] можно не указывать время глагола.
    • Свойства, указанные позже, затирают свойства, указанные ранее. Например, во фразе [красивого|hero] [hero|вн] не будет установлен винительный падеж прилагательного, так как он заменится именительным падежом переменной hero.
    • Перечень свойств слов можно найти на страницах библиотеки в github и pypi.

    Пример с кодом
    Требуется Python 3

    Установка

    pip install utg
    
    python -m unittest discover utg
    

    Код.

    from utg import relations as r
    from utg import dictionary
    from utg import words
    from utg import templates
    from utg import constructors
    
    #######################################
    # описываем существительное для словаря
    #######################################
    
    coins_forms = [# единственнео число
                   'монета', 'монеты', 'монете', 'монету', 'монетой', 'монете',
                   # множественное число
                   'монеты', 'монет', 'монетам', 'монеты', 'монетами', 'монетах',
                   # счётное число (заполнено для пример,
                   # может быть заполнено методом autofill_missed_forms)
                   'монеты', 'монет', 'монетам', 'монеты', 'монетами', 'монетах']
    
    # свойства: неодушевлённое, женский род
    coins_properties = words.Properties(r.ANIMALITY.INANIMATE, r.GENDER.FEMININE)
    
    # Для создания слова указывается его тип, формы и свойства
    coins_word = words.Word(type=r.WORD_TYPE.NOUN,
                            forms=coins_forms,
                            properties=coins_properties)
    
    # Формы слова должны быть указаны в фиксированном порядке.
    # Если вы хотите автоматизировать создание слов,
    # найти порядок форм слова и их свойства можно в переменных:
    # - utg.data.WORDS_CACHES
    # - utg.data.INVERTED_WORDS_CACHES
    
    ##############################
    # описываем глагол для словаря
    ##############################
    
    # описываем только нужны нам формы слова
    # (порядок важен и определён в utg.data.WORDS_CACHES[r.WORD_TYPE.VERB])
    action_forms = (['подарить', 'подарил', 'подарило', 'подарила', 'подарили'] +
                    [''] * 15)
    
    # свойства: совершенный, прямой залог
    action_properties = words.Properties(r.ASPECT.PERFECTIVE, r.VOICE.DIRECT)
    
    action_word = words.Word(type=r.WORD_TYPE.VERB,
                             forms=action_forms,
                             properties=action_properties)
    
    # заполняем пропущенные формы на основе введённых (выбираются наиболее близкие)
    action_word.autofill_missed_forms()
    
    ##############################################
    # создаём словарь для использования в шаблонах
    ##############################################
    
    test_dictionary = dictionary.Dictionary(words=[coins_word, action_word])
    
    ################
    # создаём шаблон
    ################
    template = templates.Template()
    
    # externals — внешние переменные, не обязаны быть в словаре
    template.parse('[Npc] [подарил|npc] [hero|дт] [coins] [монета|coins|вн].',
                   externals=('hero', 'npc', 'coins'))
    
    ##############################
    # описываем внешние переменные
    ##############################
    
    hero_forms = ['герой', 'героя', 'герою', 'героя', 'героем', 'герое',
                  'герои', 'героев', 'героям', 'героев', 'героями', 'героях',
                  'герои', 'героев', 'героям', 'героев', 'героями', 'героях']
    
    # свойства: одушевлённый, мужской род
    hero_properties = words.Properties(r.ANIMALITY.ANIMATE, r.GENDER.MASCULINE)
    
    hero = words.WordForm(words.Word(type=r.WORD_TYPE.NOUN,
                                     forms=hero_forms,
                                     properties=hero_properties))
    
    npc_forms = ['русалка', 'русалки', 'русалке', 'русалку', 'русалкой', 'русалке',
                 'русалки', 'русалок', 'русалкам', 'русалок', 'русалками', 'русалках',
                 'русалки', 'русалок', 'русалкам', 'русалок', 'русалками', 'русалках']
    
    # свойства: одушевлённое, женский род
    npc_properties = words.Properties(r.ANIMALITY.ANIMATE, r.GENDER.FEMININE)
    
    npc = words.WordForm(words.Word(type=r.WORD_TYPE.NOUN,
                                    forms=npc_forms,
                                    properties=npc_properties))
    
    ##########################
    # осуществляем подстановку
    ##########################
    
    result = template.substitute(externals={'hero': hero,
                                            'npc': npc,
                                            'coins': constructors.construct_integer(125)},
                                 dictionary=test_dictionary)
    
    ##########################
    # Проверяем
    ##########################
    
    result == 'Русалка подарила герою 125 монет.'
    

    О словарях


    Как вы могли заметить, UTG требует формирование словаря. Делается это «руками» так как в момент разработки:

    • Общедоступных качественных морфологических словарей я не нашёл.
    • Библиотека pymorphy была ещё первой версии и довольно часто косячила (особенно с винительным падежом), из-за чего пришлось от неё отказаться.

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

    Итого


    Надеюсь, библиотека окажется полезной.

    Если у вас есть идеи по её развитию (а ещё лучше, желание в нём участвовать) — пишите в личку, делайте pull requests, постите баги на гитхаб.

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

    Полезная ли штука?

    • +12
    • 3,4k
    • 8
    Поддержать автора
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 8

      0

      Не смотрели в сторону OpenCorpora для словаря (http://opencorpora.org/dict.php)? Там очень большое количество слов уже размечены и указаны их всевозможные формы.

        0
        Знаю о таком словаре.

        Если память мне не изменяет, он появился немного позже того момента, как я сделал редактирование словаря непосредственно в игре, после чего я закончил большие эксперименты со словарями и генерацией текста в целом.
        0
        [username1] зазевался и [username2] что есть сил врезал в область паха соперника. В результате чего [username1] получил травму: закрытый перелом <вырезано цензурой>
          0
          В квестовом движке QSP вроде бы есть встроенная библиотека, похожая на вашу. Если честно думал, увидеть прям генератор рассказов. Допустим в игре каждый день нужен свежий выпуск газеты, с фейковыми новостями.
            0
            Если честно думал, увидеть прям генератор рассказов.

            Специфика немного не та — большие тексты читать никто не будет.

            При создании героя, генерируется его биография. Вот такая примерно:

            Когда-то давно, в дварфовских горах, родилась девочка Тара.

            Отец Тары был ремесленником, самым обычным горожанином. И дочь воспитывал вполне соответствующие – хорошей девочкой, которая стала бы хорошей невестой: доброй, заботливой, честной и прилежной… Но судьба распорядилась по-своему.

            Юная Тара боролась за сердце богатого мужчины. Конкуренток было много, и одна из них написала в стражу донос на Тару — якобы та занимается демонологией и некромантией без разрешения властей… Тара действительно немного владела магией, но в упомянутом никогда не была виновна. Однако, стража убедила суд в обратном. Когда девушка воскресилась после позорной публичной казни, оказавшись бессмертной героиней, то узнала, что мужчина тот уже сыграл свадьбу — взял в жёны другую. А написавшая донос пропала неведомо куда. Таре следовало забыть о прежней жизни. Отныне её ждала совершенно особенная судьба.


            Создаётся из трёх независимых кусков, каждый из которых подбирается под параметры героя.

            Генерировать что-то бОльшее особого смысла нет, так как шаблонность игроки всё-равно видеть будут.
            0
            а не думали попробовать марковские цепи?
              0
              Вы их для определения формы слова предлагаете или для полной генерации текстов?

              Если для полной генерации, то это будет издевательство над пользователями — ничего осмысленного они не дадут.

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

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

            Самое читаемое