LoginSignup
1
0

More than 3 years have passed since last update.

messagen: 制約に基づくメッセージ生成ツール

Posted at

TL;DR

messagenはyamlかgolangで定義したテンプレートと制約に基づいてメッセージを生成するツールです。
具体的に何ができるのかは、利用例をご覧ください✌️

Intro

皆さんは診断メーカーをご存知でしょうか。Twitterで以下のようなツイートを一度は見かけたことがあると思います。

ところでこの診断メーカー、遊んだことがある方は多いと思いますが、診断を作ったことはありますか?
診断メーカーの診断作成画面では、以下のように「診断結果基本テキスト」から「リスト」を参照して作っていく仕組みになっています。

スクリーンショット 2019-09-22 18.43.09.png

messagenは診断メーカーのように、テンプレートとリストからメッセージを生成するツールです。YAMLかGo言語で定義を記述することで、メッセージを生成します。
例えば、診断メーカーのサンプルである好きなフルーツを表示する例は、messangenでは以下のように書くことができます。

Definitions:
  - Type: Root
    Templates: 
      - |-
        {{.User}}さんにオススメの果物は
        「{{.Fruit}}」
        です
  - Type: Fruit
    Templates: ["りんご", "みかん", "バナナ"]
$ messagen -f def.yaml --state User=Bob
Bobさんにオススメの果物は
「りんご」
です

診断メーカーと異なるのは、選択されるリスト(messagenでは定義と呼びます)に対して制約をかけられる点です。例えば、Bobはりんごが嫌いなので、それ以外から選ばなければならないとします。この場合、以下のように制約をかけることで、UserがBobの場合りんごが選択されなくなります。(詳細な記法については後ほど説明します。)

Definitions:
  - Type: Root
    Templates: 
      - |-
        {{.User}}さんにオススメの果物は
        「{{.Fruit}}」
        です
  - Type: Fruit
    Templates: ["みかん", "バナナ"]
  - Type: Fruit
    Templates: ["りんご"]
    # UserがBobである場合、りんごは選択されない
    Constraints: {"User!": "Bob"}

messagenで具体的にどのようなことができるのか知りたい方は利用例を先にご覧ください。

背景

messagenが診断メーカーや他のツールと比較してより多機能であることは、より複雑であることも意味します。「制約」は、この複雑さを許容可能にするほど重要な概念なのでしょうか?
この点についてご説明するために、私がmessagenを作り始めた背景についてご紹介します。

私はスタバ警察botというのを作っています。これはリプライで送られてきた画像がスタバかどうかを判定し、結果に応じてメッセージを返すというものです。

このメッセージは、判定結果に応じたテンプレートを元に、微妙に言い回しを毎回ランダムに生成しています。まさに診断メーカーと同じような機能が必要です。しかし加えて、次のような機能も必要であることがわかりました。

  • 排他的なメッセージの組み合わせを定義できる
    • 「一人称」の名前は「名前」だ。というメッセージで、一人称が「俺」の場合、名前は男性名だけが選ばれてほしいことがあります。(誰でも好きな一人称を使えば良いと思いますが、あくまで例です)
    • デバッグモードの時はメッセージをガラッと変える、とかもやりたくなります
  • 選択される単語の確率を変えたい

    • 10回に1回しか選ばれないレアメッセージとかですね
  • 生成されたメッセージのバリデーション

    • Twitterだと生成したメッセージの文字数が140文字を超えると困ります
  • 動的な値の代入ができる

    • スタバ警察だと、画像の判定確率をメッセージに含めたくなります
  • 言語からライブラリとして利用できる

    • スタバ警察はGo言語で実装されているので、メッセージ生成ライブラリもGo言語から呼び出したいです

このような要求を満たすため、messagenが生まれました。

詳説 messagen

冒頭のYAML定義について詳しく見てみましょう。

Definitions:
  - Type: Root
    Templates: 
      - |-
        {{.User}}さんにオススメの果物は
        「{{.Fruit}}」
        です
  - Type: Fruit
    Templates: ["りんご", "みかん", "バナナ"]

messagenでは、定義(Definition)の集合でメッセージを生成します。
定義はTypeとTemplatesを持ちます。TypeはそのDefinitionを参照する際のIDのようなものです。
TemplatesはそのDefinitionが選ばれた時に実際に生成するメッセージです。複数ある場合はランダムに選択されます。(正確にはPickerにより制御されるのですが、詳細は後ほど)
もしTemplate内部で他のDefinition Typeを参照している場合、先にそのDefinitionからメッセージを生成します。つまりDefinitionのツリー構造を深さ優先で解決していくわけです。上記の例では、「Root」というTypeを持つ定義のTemplateで「User」と「Fruit」Typeが参照されています。Fruit定義は3つのTemplateを持っているので、この中からランダムに一つが選ばれます。Userはどこにも定義がないので、実行時に与える前提です。
選択された定義とそれにより生成されたメッセージの集合をStateと呼びます。

messagenコマンドで上記yamlを実行すると、ランダムにメッセージが表示されます。
stateフラグを用いることで初期Stateを指定できます。ここではUserだけを与えていますが、カンマ区切りで複数与えることも可能です。

$ messagen -f def.yaml --state User=Bob
Bobさんにオススメの果物は
「りんご」
です

また、messagenはgolangのライブラリとしても使えます。
この場合、上記の例は次のように書くことができます。

func main() {
  rand.Seed(42)
    generator, _ := messagen.New()

    definitions := []*messagen.Definition{
        {
            Type: "Root",
            Templates: []string{"{{.User}}さんにオススメの果物は\n「{{.Fruit}}」\nです"},
        },
        {
            Type: "Fruit",
      Templates: []string{"りんご", "みかん", "バナナ"}
        },
  }

  initialState := map[string]string{"User": "Bob"}

    generator.Add(definitions...)
  messages, _ := generator.Generate("Root", initialState, 1)
  print(messages[0])
}

定義の重み付け

Weightキーでその定義が選択される確率を設定できます。同じTypeを持つ定義は幾つでも定義できるので、特定の定義だけ選ばれる確率を変えることができます。

definitions := []*messagen.Definition{
        {
            Type: "Fruit",
      Templates: []string{"りんご", "みかん", "バナナ"}
        },
    {
      Type: "Fruit",
      Templates: []string{"ドラゴンフルーツ"}
      Weight: 0.1 // 1/10の重みになる。デフォルトは1なので、この例では0.1/(1+0.1)の確率で選ばれる
    },
  }

定義ごとの制約

messagen最大の特徴が冒頭でも述べた定義の制約(Constraints)です。
各定義には、現在のStateを元に選ばれても良いかどうかの制約を与えることができます。
例えば、一人称に応じて名前を出し分ける例は以下になります。

Definitions:
  - Type: Root
    Templates: ["{{.Pronoun}}の名前は{{.Name}}。"]
  - Type: Pronoun
    Templates: ["あたし", "俺"]
  - Type: Name
    Templates: ["太郎", "次郎"]
    Constraints: {"Pronoun": "俺"}
  - Type: Name
    Templates: ["花子", "佳子"]
    Constraints: {"Pronoun": "あたし"}

この例では、一人称(Pronoun)に応じて、選択されるNameが決定されます。
例えば、上から3つ目のNameでは、ConstraintsでPronounが"俺"であることが要求されているため、"あたし"の場合は選択されません。

Constraints Operator

Constraintsでは、キーの最後に記号を付けることで特殊な挙動をさせることができます。

Operator 意味
? キーが存在しなくても選択可能になる
+ ?に加えて、存在しなかった場合はキーと値を追加する
! 指定された値でない場合に選択可能になる
/ 値を正規表現として解釈し、マッチすれば選択可能になる
:[Priority] プライオリティを指定する(後述)

例えば、先ほどの例で一人称のバリエーションを増やしたいとします。素朴には以下のように、Pronounを正規表現でマッチさせることができます。

Definitions:
  - Type: Root
    Templates: ["{{.Pronoun}}の名前は{{.Name}}です"]
  - Type: Pronoun
    Templates: ["あたし", "わたくし", "俺", "わし"]
  - Type: Name
    Templates: ["太郎", "次郎"]
    Constraints: {"Pronoun/": "俺|わし"}
  - Type: Name
    Templates: ["花子", "佳子"]
    Constratins: {"Pronoun/": "あたし|わたくし"}

が、一人称がもっと増えてくると破綻の予感がします。
そこで代わりに+operatorを使ってみます。

Definitions:
  - Type: Root
    Templates: ["{{.Pronoun}}の名前は{{.Name}}です"]
  - Type: Pronoun
    Templates: ["あたし", "わたくし"]
    Constraints: {"Gender+": "女"}
  - Type: Pronoun
    Templates: ["俺", "わし"]
    Constraints: {"Gender+": "男"}
  - Type: Name
    Templates: ["太郎", "次郎"]
    Constraints: {"Gender": "男"}
  - Type: Name
    Templates: ["花子", "佳子"]
    Constratins: {"Gender": "女"}

こうすると、一人称を増やしても他の定義には影響を与えません。

制約の優先度

食べ物の説明文を考えます。

Definitions:
  - Type: Root
    Templates: ["この{{.Food}}は{{.Description}}です"]
  - Type: Food
    Templates: ["ケーキ", "ハンバーグ", "オムライス"]
  - Type: Description
    Templates: ["ふわふわ", "クリームたっぷり"]
    Constraints: {"Food": "ケーキ"}
  - Type: Description
    Templates: ["美味しそう"]
$ messagen run -f def.yaml
このケーキは美味しそうです

一番下のDescriptionは制約がないため、どんなFoodが選ばれても使われる可能性があります。
しかし、ケーキ専用のDescirptionを下から2番目の定義として作っているので、ケーキが選ばれたら必ずこの定義を使いたいとします。このような時はConstraints priorityを使います。

Definitions:
  - Type: Root
    Templates: ["この{{.Food}}は{{.Description}}です"]
  - Type: Food
    Templates: ["ケーキ", "ハンバーグ", "オムライス"]
  - Type: Description
    Templates: ["ふわふわ", "クリームたっぷり"]
    Constraints: {"Food:1": "ケーキ"} # キーの:1がConstraints Priority
  - Type: Description
    Templates: ["美味しそう"]

Type:[PriorityNum]と書くと、その制約にマッチした場合にプライオリティ分の値が加算されます。候補の定義のうち、最大のプライオリティを持つ定義が選択されます。(同じ値の場合は、Weightに従ってランダムに選ばれます)
プライオリティを明示しなかった場合は0として扱われます。
上記の例でFoodとしてケーキが選ばれた場合、上のDescriptionはプライオリティ1、下はプライオリティ0なので、必ず上のDescriptionが選ばれます。

定義の解決順

食べ物とその味の表現を表すテンプレートを考えます。

Definitions:
  - Type: Root
    Templates: ["{{.Adjective}}な{{.Food}}"]
  - Type: Adjective
    Templates: ["ふわふわ", "クリームたっぷり"]
    Constraints: {"FoodGenre+": "ケーキ"}
  - Type: Adjective
    Templates: ["ジューシー", "ボリューミー"]
    Constraints: {"FoodGenre+": "肉"}
  - Type: Food
    Templates: ["いちごケーキ", "モンブラン", "チーズケーキ", "ガトーショコラ"]
    Constraints: {"FoodGenre": "ケーキ"}
  - Type: Food
    Templates: ["ハンバーグ", "ステーキ"]
    Constraints: {"FoodGenre": "肉"}
    Weight: 0.5 # ここで指定しても、先にAdjectiveが選ばれるため意味がない

ケーキは4種類あるのに対して、肉は2種類しかないので、肉の登場確率をWeightを0.5にしました。しかし、テンプレート内の定義はデフォルトでは出てきた順に解決されるので、先にAdjectiveがそれぞれ同じ確率で選ばれてしまいます。肉の登場確率を下げるには、肉に関連するAdjectiveへWeightを与える必要があります。しかし、Weightを与えたい定義と実際にWeightを書く定義が離れていると混乱しますし、もっと複雑な定義になると破綻しそうです。
定義の解決順をOrderで指定すれば、この問題を解決することができます。

Definitions:
  - Type: Root
    Templates: ["{{.Adjective}}な{{.Food}}"]
    Order: ["Food"] # Foodから解決される. 省略されたTypeはTemplateの前から解決される
  - Type: Adjective
    Templates: ["ふわふわ", "クリームたっぷり"]
    Constraints: {"FoodGenre+": "ケーキ"}
  - Type: Adjective
    Templates: ["ジューシー", "ボリューミー"]
    Constraints: {"FoodGenre+": "肉"}
  - Type: Food
    Templates: ["いちごケーキ", "モンブラン", "チーズケーキ", "ガトーショコラ"]
    Constraints: {"FoodGenre": "ケーキ"}
  - Type: Food
    Templates: ["ハンバーグ", "ステーキ"]
    Constraints: {"FoodGenre": "肉"}
    Weight: 0.5 # 先にFoodが選ばれるのでOK

エイリアス

一つの定義から複数のメッセージを生成したい場合があります。

Definitions:
  - Type: Root
    Templates: ["「{{.Adjective}}」かつ「{{.Adjective}}」な{{.Food}}"]
  - Type: Adjective
    Templates: ["ふわふわ", "クリームたっぷり"]
  - Type: Food
    Templates: ["いちごケーキ", "モンブラン", "チーズケーキ", "ガトーショコラ"]

しかし、定義から生成されたメッセージはStateに保存され、以降は同じ値が参照されます。なので、上記の例では、2つのAdjectiveは必ず同じ値になります。

$ messagen run -f def.yaml
「ふわふわ」かつ「ふわふわ」なモンブラン

そこで、エイリアスを用いて同じ定義から複数のメッセージを取り出します。AllowDuplicateで同じ定義から重複した値が生成されることを許可するかどうかを決められます。

Aliases:

Definitions:
  - Type: Root
    Templates: ["「{{.Adjective}}」かつ「{{.AnotherAdjective}}」な{{.Food}}"]
    Alias:
      AnotherAdjective: {"Type": "Adjective", "AllowDuplicate": false}
  - Type: Adjective
    Templates: ["ふわふわ", "クリームたっぷり"]
  - Type: Food
    Templates: ["いちごケーキ", "モンブラン", "チーズケーキ", "ガトーショコラ"]
$ messagen run -f def.yaml
「ふわふわ」かつ「クリームたっぷり」なモンブラン

Picker & Validator

messagen組み込みの制約では表現できないメッセージ生成ルールを適用したい場合があります。例えば、特定の単語を含むテンプレートを優先して利用する、twitterに投稿するために文字数を140文字以下にするなどです。
これらを実現するために、picker / validatorという仕組みがあります。

pickerは定義やテンプレートを選択する際の挙動を制御するための関数で、以下のようなシグネチャになっています。

type TemplatePicker func(def *DefinitionWithAlias, state *State) (Templates, error)
type DefinitionPicker func(defs *Definitions, state *State) ([]*Definition, error)

それぞれ与えられた定義とStateから定義やテンプレートのフィルタ/並び替えを行い、戻り値として返します。

TemplatePicker

TemplatePickerは、ある定義が持つテンプレートリストから、どれをどの順番で取り出すかを制御するための関数です。
例えば"ん"を含むテンプレートを選択しないTemplatePickerは以下のように実装できます。

func IgnoreNTemplatePicker(def *DefinitionWithAlias, state *State) (Templates, error) {
    var newTemplates Templates
    for _, template := range def.Templates {
        msg, _, err := template.ExecuteWithIncompleteState(state)
        if err != nil {
            return nil, err
        }
        if !strings.Contains(string(msg), "ん") {
            newTemplates = append(newTemplates, template)
        }
    }
    return newTemplates, nil
}

ExeuteWithIncompleteStateは、テンプレートが要求するTypeがStateに存在しなくても空文字として評価してメッセージを生成するメソッドです。例えば{{.A}} and {{.B}}!というテンプレートでStateが{"A": "V1"}だった場合、ExeuteWithIncompleteStateの結果はV1 and !となります。詳細はmessagenのgodoc(準備中)をご覧ください。

DefinitionPicker

DefinitionPickerはあるDefinition Typeをもつ定義からどれをどの順番で取り出すかを制御するための関数です。
例えば、messagen内部で、ある定義が制約を満たしているかどうかのチェックは、以下のようなDefinitionPickerとして実装されています。

func ConstraintsSatisfiedDefinitionPicker(definitio ns *Definitions, state *State) ([]*Definition, error) {
    var newDefinitions Definitions
    for _, def := range *definitions {
        if ok, err := def.CanBePicked(state); err != nil {
            return nil, err
        } else if ok {
            newDefinitions = append(newDefinitions, def)
        }
    }
    return newDefinitions, nil
}

def.CanBePickedは、stateが制約を満たしているかどうかチェックする関数です。

TemplateValidator

TemplateValidatorは、あるTemplateに対するstateがvalidかどうかを判定し、booleanを返す関数です。falseを返した場合、現在着目しているテンプレート以降の探索を打ち切ります。
TemplatePickerに似ていますが、stateが変更されるたびに呼び出されるので、適切に実装することで木構造を探索する際の枝刈りを行うことができます。例えばmessagenには、以下のような合計文字数をチェックするvalidatorのgeneratorであるMaxStrLenValidatorがあらかじめ用意されています。

func MaxStrLenValidator(maxLen int) TemplateValidator {
    return func(template *Template, state *State) (bool, error) {
        incompleteMsg, _, err := template.ExecuteWithIncompleteState(state)
        if err != nil {
            return false, err
        }
        return utf8.RuneCountInString(string(incompleteMsg)) <= maxLen, nil
    }
}

利用例

ここではmessagenがどのようなメッセージ生成を行えるのかを紹介します。

りょうくんグルメっぽい例

本家の方はちゃんと食べ物に合う表現を使っておられると思いますが、ここではジャンルに合った形容詞ならなんでも良いというルールで作ってみます。

Definitions:
 - Type: Root
   Templates: 
     - |-
       まじでこの世の全ての{{.FoodCategory}}好きに教えてあげたいんだが
       {{.Location}}には全ての人間を虜にする禁断の{{.FoodName}}がある
       これが{{.FoodDescription}}で超絶美味いからぜひ全国の{{.FoodCategory}}好き、
       {{.FoodCategory}}を愛する者たち、{{.FoodCategory}}を憎む者たち、全ての{{.FoodCategory}}関係者に伝われ
   OrderBy: ["FoodName"]

 - Type: FoodName
   Templates: ["サーロインステーキ", "リブステーキ"]
   Constraints: {"FoodCategory+": "肉", "Location+": "品川のhogeビル"}

 - Type: FoodName
    Templates: ["fugaハンバーグ"]
    Constraints: {"FoodCategory+": "肉", "Location+": "丸の内のfugaビル"}

 - Type: FoodName
    Templates: ["いちごケーキ", "モンブラン", "ガトーショコラ"]
    Constraints: {"FoodCategory+": "ケーキ", "Location+": "表参道のpiyoビル"}

 - Type: FoodDescription
    Templates: ["ジューシー", "ボリューミー"]
    Constraints: {"FoodCategory": "肉"}

 - Type: FoodDescription
   Templates: ["{{.CakeDescription}}かつ{{.AnotherCakeDescription}}"]
   Constraints: {"FoodCategory": "ケーキ"}
   Alias:
     AnotherCakeDescription: {"Type": CakeDescription, "AllowDuplicate": false}

 - Type: CakeDescription
   Templates: ["ふわふわ", "濃厚", "クリームたっぷり"]
$ messagen run -f gurume.yaml
まじでこの世の全てのケーキ好きに教えてあげたいんだが
表参道のpiyoビルには全ての人間を虜にする禁断のモンブランがある
これが濃厚かつふわふわで超絶美味いからぜひ全国のケーキ好き、
ケーキを愛する者たち、ケーキを憎む者たち、全てのケーキ関係者に伝われ

ガチャを作る例

診断メーカーでよく見るやつです。本家ではレアリティに応じて同じ文章をコピペしまくると言う荒技が用いられますが、messagenではWeightを用いてシンプルに表現できます。

Definitions:
  - Type: Root
    Templates:
      - |-
        フルーツ10連ガチャの結果↓
        {{.Item}}
        {{.Item2}}
        {{.Item3}}
        {{.Item4}}
        {{.Item5}}
        {{.Item6}}
        {{.Item7}}
        {{.Item8}}
        {{.Item9}}
        {{.Item10}}
    Aliases:
      Item2: &item {"Type": "Item", "AllowDuplicate": true}
      Item3: *item
      Item4: *item
      Item5: *item
      Item6: *item
      Item7: *item
      Item8: *item
      Item9: *item
      Item10: *item
  - Type: Item
    Templates: ["SR[ドラゴンフルーツ]", "SR[マンゴー]"]
    Weight: 0.1
  - Type: Item
    Templates: ["R[メロン]", "R[スイカ]"]
    Weight: 0.4
  - Type: Item
    Templates: ["N[りんご]", "N[みかん]", "N[バナナ]"]
$ messagen run -f gatya.yaml
フルーツ10連ガチャの結果↓
N[みかん]
N[みかん]
N[バナナ]
R[メロン]
N[バナナ]
R[メロン]
N[りんご]
R[メロン]
SR[マンゴー]
N[りんご]

キャッチコピーを生成する例

コピーメカ/キャッチコピー自動作成サイトをリスペクトした例です。本家同様、与えられたジャンルに応じて適切なキャッチコピーを生成します。

Definitions:
  - Type: Root
    Templates:
      - "無人島に持って行きたい{{.Product}}"
      - "{{.Product}}を{{.IfAction}}、自信が生まれる"
      - "{{.Product}}を{{.Action}}前に知っておいて欲しいこと"

  - Type: Action
    Templates: ["食べる"]
    Constraints: {"Genre": "Food"}
  - Type: Action
    Templates: ["買う"]
    Constraints: {"Genre": "Other"}

  - Type: IfAction
    Templates: ["食べれば"]
    Constraints: {"Genre": "Food"}
  - Type: IfAction
    Templates: ["買えば"]
    Constraints: {"Genre": "Other"}
$ messagen run -f copy.yaml --state Product=ラーメン,Genre=Food
ラーメンを食べれば、自信が生まれる

ポケモン名でいろは歌を作る例

制約によるメッセージ生成の極端な例として、こちらのブログで紹介されているような、ポケモン名でいろは歌を作る例を考えます。つまり、46文字を一度ずつだけ使ったポケモン名の列挙です。ただし、濁音/半濁音は取り除き()、長音は無視します。また、今回はズルをして、ケルディオとしています。

Definitions:
- Type: Root
   Templates: 
     - {{P1}} {{P2}} {{P3}} {{P4}} {{P5}} {{P6}} {{P7}} {{P8}} {{P9}} {{P10}} {{P11}} {{P12}} {{P13}}
   Alias:
     P1: &pokemon_alias {"Type": "Pokemon", "AllowDuplicate": false}
     P2: *pokemon_alias
     P3: *pokemon_alias
     P4: *pokemon_alias
     P5: *pokemon_alias
     P6: *pokemon_alias
     P7: *pokemon_alias
     P8: *pokemon_alias
     P9: *pokemon_alias
     P10: *pokemon_alias
     P11: *pokemon_alias
     P12: *pokemon_alias

 - Type: Pokemon
   Templates: ["フシギダネ", "ヒトカゲ", "ゼニガメ", "ヌオー", "ソーナノ",
                "キャタピー", "トランセル", "バタフリー", "ビードル", "コクーン", "スピアー",
                "ペロリーム", "チョボマキ", "ハブネーク", "ワタッコ",
                "ミュウ", "エレザード", "モンジャラ", "ケルディヲ",
    ]

次にValidatorを定義します。

func IrohaTemplateValidator(template *messagen.Template, state *messagen.State) (bool, error) {
    incompleteMsg, _, err := template.ExecuteWithIncompleteState(state)
    if err != nil {
        return false, err
    }
    return !HasDuplicatedRune(NormalizeKatakanaWord(string(incompleteMsg))), nil
}

HasDuplicatedRuneで、文字の重複がないかをチェックしています。また、NormalizeKatakanaWordで、濁音のハンドリング等を行っています。コード全体を記載すると長くなってしまうので、ご興味がある方はソースをご覧ください。

このvalidatorを適用したmessagenを実行します。

func main() {
    opt := &messagen.Option{
        TemplateValidators: []messagen.TemplateValidator{IrohaTemplateValidator},
    }
    generator, _ := messagen.New(opt)
    config, _ := messagen.ParseYamlFile("examples/iroha/pokemon.yaml")
    generator.AddDefinition(config.Definitions...)
    msg, _ := generator.Generate("Root", nil, 1)
    fmt.Println(msg)
}

実行すると以下のようになります。

$ go run main.go
[ワタッコ ペロリーム チョボマキ ミュウ モンジャラ ケルディヲ ゼニガメ ハブネーク ソーナノ ヌオー スピアー エレザード]

同じ文字を一度ずつ46文字使ったポケモン名の列挙ができていることがわかります。

(なお、実際にはもっと効率的ないろは歌生成アルゴリズムが存在します。もし高速にいろは歌を生成したくて仕方がないという奇特な方がいらっしゃれば、高速いろは歌生成CLIツールもご覧ください😃)

スタバ警察の例

最後に、messagenを作るきっかけとなったスタバ警察の例です。スタバ警察では以下の要求がありました。

  • デバッグモードではデバッグメッセージを表示したい
  • 画像の判定結果の確信度に応じてメッセージを出し分けたい
  • (Twitterのbotなので)合計文字数を140字以下にしたい
    • これはTemplateValidatorで紹介したMaxStrLenValidatorで実現できます
  • 低確率で出現するメッセージを作りたい

以下はこれらを実現するyamlです。

Definitions:
  - Type: Root
    Templates:
      - |-
        ピピーッ❗️🔔⚡️スタバ警察です❗️👊👮❗️
        {{.TweetCheck}}{{.SutabaDescription}}スタバ❗️❗️{{.GoodEmoji}}
        {{.LastMessage}}
    Constraints: {"Class": "Sutaba"}

  - Type: Root
    Templates:
      - |-
        ピピーッ❗️🔔⚡️スタバ警察です❗️👊👮❗️
        アナタのツイート💕は❌スタバ法❌第{{.RuleNum}}条🙋
        「スタバぢゃないツイートをスタバなうツイート💕してゎイケナイ❗️」
        に違反しています😡今スグ消しなサイ❗️❗️❗️❗️✌️👮🔫
    Constraints: {"Class": "Other", "Confidence/": "High|Medium"}

  - Type: Root
    Templates: ['{"class": "{{.Class}}", "confidence": "{{.Confidence}}"}']
    Constraints: {"Debug:1": "on"}

  - Type: TweetCheck
    Templates:
      - "{{.Exclamation}}このツイート{{.ThinkingEmoji}}{{.ThinkingEmoji}}{{.ThinkingEmoji}}..."

  - Type: TweetCheck
    Templates: ["アアーーー❗️なんだこれはーーー❗️❗️"]
    Weight: 0.5

  - {"Type": "Exclamation", "Templates": ["ムムッ", "ヤヤッ", "オオッ"]}
  - {"Type": "ThinkingEmoji", "Templates": ["🤔", "🤨"]}
  - {"Type": "GoodEmoji", "Templates": ["😆", "😂"]}

  - Type: SutabaDescription
    Templates: ["完全に", "間違いなく"]
    Constraints: {"Confidence": "High"}

  - Type: SutabaDescription
    Templates: ["おそらく", "多分"]
    Constraints: {"Confidence": "Medium"}

  - Type: LastMessage
    Templates:
      - "この調子でグッドなスタバツイートを心がけるようにッ❗️👮‍👮‍"
      - "市民の協力に感謝するッッッ👮‍👮‍❗"
$ messagen run -f sutaba.yaml -s Confidence=High,Class=Sutaba,Debug=off
ピピーッ❗️🔔⚡️スタバ警察です❗️👊👮❗️
オオッこのツイート🤔🤔🤔...間違いなくスタバ❗️❗️😆
市民の協力に感謝するッッッ👮‍👮‍❗

$ messagen run -f sutaba.yaml -s RuleNum=999,Confidence=High,Class=Other,Debug=off
ピピーッ❗️🔔⚡️スタバ警察です❗️👊👮❗️
アナタのツイート💕は❌スタバ法❌第999条🙋
「スタバぢゃないツイートをスタバなうツイート💕してゎイケナイ❗️」
に違反しています😡今スグ消しなサイ❗️❗️❗️❗️✌️👮🔫

$ messagen run -f sutaba.yaml -s Confidence=High,Class=Other,Debug=on
{"class": "Sutaba", "confidence": "High"}

おわり

messagenについて、背景、利用方法、利用例をご紹介しました。
もしご興味があればぜひ使って見てください😃
まだドキュメントが足りない部分もあるので、不明点などあればTwitterでお気軽にご質問ください

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0