えんとつ町のプペルの紹介記事をマルコフ連鎖で生成して19円貰う

『えんとつ町のプペル』という映画をご存知でしょうか？
西野某氏が製作総指揮・脚本・原作を務める大ヒット上映中の映画です

大好評の映画なので、crowdworksには[このような](https://crowdworks.jp/public/jobs/6079013)レビュー記事の募集も行われていました
(現在は削除されています)

<a href="https://web.archive.org/web/20210130080548/https://crowdworks.jp/public/jobs/6079013">
  <img width="360" src="https://user-images.githubusercontent.com/43836584/108028877-dad4ec00-706f-11eb-88ac-69702160144f.png">
</a>

もちろん報酬目当てではないのですが「プペルが面白かったから、ぜひみんなにシェアしたい」とあるように宣伝でき、お金まで貰えるこの仕事、乗るしかない！
ですが文章を書くのは苦手なので優秀なプペラーの方々の感想からレビューを生成しようと思います

## 作ったもの

[![ivgtr/poupeteer - GitHub](https://gh-card.dev/repos/ivgtr/poupeteer.svg)](https://github.com/ivgtr/poupeteer)

- [公開サイト](https://poupelle.ivgtr.vercel.app/)

## 使用技術
- TypeScript
- Next.js

## 実装
流れとしては、

- Twitterから`#えんとつ町のプペル`を含むツイートを取得し、保存
- 分かち書きし扱いやすいように加工して保存、Github Action上でcronを回して少しづつデータを増やしていく
- クライアントで保存したデータをGETし、マルコフ連鎖を使い文章を生成しては目標の2000文字を超えるまで繰り返す

> マルコフ連鎖は、未来の挙動が現在の値だけで決定され、過去の挙動と無関係である。各時刻において起こる状態変化（遷移または推移）に関して、マルコフ連鎖は遷移確率が過去の状態によらず、現在の状態のみによる系列である

### 1.1 データを取得

ツイッターのデータ取得に関してはたくさん参考になる記事があるので探してください

やったこととしては、`search/tweets` APIを使い、`#えんとつ町のプぺル`を検索します
その際に、queryに` exclude:retweets`を付けることでRTを除外する事ができます
また、`result_type`を`recent`にする事で時系列に取得でき、`trim_user: true`と`include_entities: false`を指定する事で不要な情報を削る事ができます

### 1.2 データを加工

`tiny-segmenter`モジュールを使う事で簡単に文章の分かち書きができます
要らない情報を除去し、`tiny-segmenter`に読み込ませ、分かち書きされた文章を配列にまとめます

```ts
const tinySegmenter = new TinySegmenter()

const tmp = tweetData.reduce<string[][]>((acc, value) => {
  const text = value.text
    .replace(/@([A-Za-z0-9_]+)\s+/g, '')
    .replace(/[#＃][Ａ-Ｚａ-ｚA-Za-z一-鿆0-9０-９ぁ-ヶｦ-ﾟー._-]+/g, '')
    .replace(/https?:\/\/[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+/g, '')
    .replace(/\r?\n/g, '')
    .trim()
    .replace(/\s+/g, '')
    .split('。')
    .join('')

  const segments: string[] = tinySegmenter.segment(text)
  acc.push(segments)
  return acc
}, [])

// console.log(tmp) -> [["私","は",...],["おでん","を",...],...]
```

分かち書きされた文章をマルコフ連鎖で使う為の辞書データへ加工します
今回はささっと19円を手に入れる為に、2-gram単位でオブジェクトに登録していきます

> 任意の文字列や文書を連続したn個の文字で分割するテキスト分割方法．特に，nが1の場合をユニグラム（uni-gram），2の場合をバイグラム（bi-gram），3の場合をトライグラム（tri-gram）と呼ぶ．

```ts
const db: {[key: string]: string[]} = {}

tmp.map((tweet) => {
  ;['_BOS_', ...tweet, '_EOS_'].sort((a: string, b: string) => {
    db[b] ? (db[b] = db[b].concat([a])) : (db[b] = [a])
    return 1
  })
})

// console.log(tmp) -> {"私":["は","の",...],"は":["おでん","私",...],...}
```

文頭には`_BOS_`を、文末には`_EOS_`を付与し、文章を生成する際に始まりと終わりを判別させます
これで辞書データは完成です

`fs.writeFileSync`で書き出し、github上にDB含め、pushしておきます

### 2.1 辞書データを取得

github上にpushされてるファイルには`https://raw.githubusercontent.com/{user_name}/{repo_name}/{branch_name}/{file_path}`でアクセスでき、GETで取得できます
client側で取得し、保持しておきます

```tsx
const [DB, setDB] = useState<{[key: string]: string[]}>({})

useEffect(() => {
  fetch('https://raw.githubusercontent.com/{user_name}/{repo_name}/{branch_name}/{file_path}')
    .then((response) => response.json())
    .then((data) => setDB(data))
}, [])
```

### 2.2 文章を生成

ボタンを押したら文章を生成し、`<textarea />`に表示させます

```tsx
const [view, setView] = useState<string>('')

const makeSentence = (text: string) => {
  let now_word = ''
  let morphemes = ''
  now_word = DB['_BOS_'][Math.floor(Math.random() * DB['_BOS_'].length)]
  morphemes += now_word
  while (now_word != '_EOS_') {
    now_word = DB[now_word][Math.floor(Math.random() * DB[now_word].length)]
    morphemes += now_word
  }
  morphemes = text + morphemes.replace(/_EOS_$/, '\n\n')
  if (morphemes.length > 2000) {
    setView(morphemes)
  } else {
    makeSentence(morphemes)
  }
}

return(
  <textarea
    placeholder="ボタンを押して生成してね"
    readOnly
    value={view}
  />
  <button onClick={() => makeSentence('')}>文章を作るよ！</button>
)
```

文章を生成し、2000文字以下であれば生成した文章を引数にもう一度実行し、足していきます
こうする事により意味不明なプペルの感想を排出していき、必要文字数まで無理やり埋めます

えんとつ町のプペルの紹介記事を書く事ができます！
試しに文章を生成してみます

```txt
台湾公開説教ブログでウルウル、ちゃんと思います😭辛い時でも下をもらえたり、えんとつ町のいろいろなもんではや絵の副音声第二弾のプル！初めて映画館で泣きました一人で泣きました…即決で、えんとつ町の映画を聴いた西野亮廣さんすごい！初めて映画館では寝る前に上を見ずにイイ！初めて映画館で泣きましたムスメと思います感動の副音声鑑賞ます😭辛い時でも下をありがとうございます😭辛い時でも下を向いていこうもファーストペンギン目指します感動の映画を見ずに上を向いて生きて生きていこうと思います感動の薄川がとうございます感動のプペルも下を向いて生きてき…………

プペル、周りに上を取られるけどそしていこうと思います感動のは、前回より、自分がとうございます😭辛い時でも下を向いてゆく…

あと、プペルでニヤニヤ観れば、指で泣きました号泣しまくりです😊

届いていこうと思います感動🥺

西野亮廣さんすごい！oh~えんとつ町の映画を見ずに行けたら観てついていこうと可能性自分のほんと思います😊

西野亮廣さんすごい！西野亮廣プぺ！nice！夢を持って生きて生きて生きてもらいました号泣しすぎて生きておいて生きてもらいました映画をありがとうございます😭辛い時でも下を見ずに感動の息子が好きに変えてきた！のんびり楽しくなった号泣しまくりです😭辛い時でも下を覚えてる描くしかなくエンドロールが離れていこうと思います感動の①日(^_^T▽^*)◆鈴木臨之介宮下恵一度変わったストーリーで泣きました本がとうございます💛プペル！夢を現すルビッチに殴られたから、2/Zip&amp;轟音&amp;hydeの方限定で泣きました号泣しまくりです😊

1回みなきゃわからやったわ絵本買ってきた第二弾もめっちゃ喋ってる時は映画を向いていこうと思います感動の友人が５回チャレンジで大人も短くていこうと思います😊

２プペ🎞️

西野亮廣さんすごい！初めて映画館で有名に上を向いて生きていこうと同じ時代をありがとうございます感動の副音声楽しみなんのリンクする部分も下を潰して生きてくれたのに上をありが一生懸命描いてる

昨日、主題歌は不思議な？馬鹿に上をありが存在する話にいいと思います!マーク煙に夢をありが世界は単純でもプペルを飾りたい！初めて映画館で泣きましたよ(*^▽T)めっちゃ使ってるの映画を向いて上映決定！初めて映画館で泣きしまくりです😭辛い時でもそっとそばにゲスト出演する人は苦手なことになるルビッチに行ったより

とオススメしまくりです😭辛い時でも下を見ずに！初めて映画館で泣きました号泣した

西野亮さんお金:110円達成率:8-キングコング西野亮廣さんすごい！初めて映画館で言えば、副音声をありがあれ？

プペルのか

西野さんすごい！初めて映画館で対談聴きたいと思いますのメッセージが最高に生きてくれることがとうございます感動のサムネの方で泣きました号泣しまくりです😭辛い時でも下を向いていこうと思います感動しまくりです😭辛い時でもダメ親父にも見てないだろうね✩.*˚HYDEさんの映画えんとつ町ので泣きましたのシーンで、諦めた号泣しまくりです😊

西野亮廣さんすごい！初めて映画館で泣きましたです😊

西野亮廣さんすごい！初めて映画館で泣きました感動の映画を見ずに上をあります😭辛い時でもあるしかし、エールをありがとうございます感動のも下をありがいいです😭辛い時でも「えんとつ町の映画を向いてよかったいろんな新事実がとうございます感動すると思います感動の体験で泣きました✨

西野さんすごい！初めて映画館で「はぁ〜疲れましたwwwwけどワクワクして音量下げちゃった本がとうございます感動する場面をありました‼️💕

西野亮廣さんすごい！みなさんも下を見ずに‼️

西野亮廣さんすごい！そしてプペルを見ずに上を向いて観れても気をありがとうございます感動の映画レビュー→ゴミ人間がとうございます😊

えんとつ町の映画を向いていこうと思います😊

西野亮廣さんに上を向いてくれる同志がとうございます感動の内容がとうございます(税抜)98回目みた第一緒に上を向いてたんだったんだっていた🎩⭐️

西野亮廣さん離島話に入って上向いてきた号泣しまくりです😊

西野亮廣さんすごい！初めて映画館で泣きました、これ！初めて映画館で泣きました号泣してる思わず最近は永遠に一緒に信じ抜く事、、まだ観て生きていこうと思います😭辛い時でも叩かれる時代に上を見ずに上を見ずには完全に感動のかよっ！！！！/18時に挑戦しまくりです😊

西野亮廣さんの教官の映画を始めと思います😊

西野様の映画をありがとうございます感動の言葉に涙が出てん、すごく共感😭辛い時でも下をありが「」！お金の秘密かぁみんなでえんとつ町の映画のダメ親父に西野亮廣さんの映画プペル観てきました
```

西野亮廣さんすごい！これで19円はあなたのものですね

## 現在の問題
- レビューの書き方指南にあったように「どこの映画館でみたか」といった情報や写真は自分で追加する必要があります
- 案件が削除されてしまいました（泣

## 終わりに

ちなみに自分は0プペしました、皆さんは何プペ目でしょう？

## 参考
- マルコフ連鎖を使って文章生成をしてみる
  - http://cedro3.com/ai/markov/
- 【えんとつ町のプペル】上映中の映画館に関する記事作成！(現在は削除されています)
  - https://crowdworks.jp/public/jobs/6079013