2023年だし日本語勉強する

# あけましておめでとうございます(*1)

2023年が明けました！素数の年ですね！(大嘘)

最近勉強をさぼりがちなので、本からインプットを受ける題材でプログラムを組んでみる。
去年影響を受けたのが

+ ゆる言語学ラジオ
+ ずんだもんのRTA

なので、「日本語xずんだもん」に関する物とする。今回は会話の相手をずんだもん化させる。

<div align="center">
<img width="300" alt="zundamon" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/b3fce11b-344b-f99b-7cda-6d762b67021c.jpeg"></img>
</div>
<div style="text-align: center;">
<font color="dark_pink">著作権が怖いので、ぼかしてます</font>
</div>

会話の中に組み込むシステムなので会話の真ん中にシステムが来るイメージ。

```mermaid
sequenceDiagram;

actor joshi as 相手
participant zunda as zunda-filter
actor buka as 俺

loop 会話
  joshi ->> zunda: 発話
  zunda ->> zunda: Zundanize 
  zunda ->> buka: ずんだ化された発話
  buka -->> joshi: 「なるほどですね!」
end
```

<font color="dark_pink">要はこう言う事。</font>

相手を直接システムにぶち込めれば良いんだけれど、人なのでインストールできない。都合がよい事に最近はリモートワークなのでオンラインミーティングを想定して相手の音声だけzunda-filterに取り込む想定とする。

```mermaid
sequenceDiagram;

actor joshi as 相手
participant meeting as ミーティングツール
participant sst as SST
participant zunda as zunda-filter
participant tts as VoiceVox
actor buka as 俺

loop 会話
  joshi ->> meeting: 発話
  meeting ->> sst: 音声
  sst ->> zunda: テキスト
  zunda ->> zunda: zundanize
  zunda ->> tts: ずんだ化テキスト
  tts ->>buka: ずんだ化説教
  buka -->> meeting: 「それ良い考えですね！」
  meeting -->> joshi: 「それ良い考えですね！」
  joshi ->> joshi: 満足
end
```

ミーティングツールは音声が取り出せれば何でも良い。

尚結果から言うと、下記をあきらめた

+ SSTからのつなぎこみ(めんどくさくなった為)
+ TTSへのつなぎこみ(めんどくさくなった為)
+ フィルタのWebインタフェース(めんどくさくなった為)

*1: 2023/01/01に投稿する予定でした。

# できあがったもの

出来上がったのは、ずんだもんムード変換フィルタ。

[miaytama/zundafilter](https://github.com/miyatama/zundafilter)

変換前

```text
私をみつけるとすぐに、弟の啓三は例のとおり大きく手を振った。
私は気がつかないふりをして、三番線のプラット・ホームのほうを見ていた。
啓三は近よって来ると、これまた例の如く私の肩を叩いた。
彼の体から香水が匂った。
「早かったね」
と啓三が云った、
「病院のほうはいいの」
私は黙って頷ずいた。
「この暮になってひどいよ、おれにとっちゃあ一時間が何万円にもつくときだからね」
と啓三はなめらかに云った、
「往復するだけでも二日だぜ、もしものことがあっても一日しきゃ日は取れないんだ、一日だな、一日以上は絶対にだめなんだ、おふくろも罪なときに罪なことをするよ」
私は黙って階段口のほうを見た。
啓三は母を責めているのではない、一時間が何万円にもつくという、自分の言葉の裏書きをしているにすぎないのだ。
「こんなところに立っていてもしようがない」
と啓三が云った、
「中へはいって坐ろうじゃないの」
「室町がまだ来ないんだ」
```

変換後

```text
ぼくをみつけるとすぐに、弟の啓三は例のとおり大きく手を振ったのだ。
ぼくは気がつかないふりをして、三番線のプラット・ホームのほうを見ていたのだ。
啓三は近よって来ると、これまた例の如くぼくの肩を叩いたのだ。
ぼくの体から香水が匂ったのだ。
「早かったね」
と啓三が云ったのだ、
「病院のほうはいいのだ」
ぼくは黙って頷ずいた。
「この暮になってひどいよ、ぼくにとっちゃあ一時間が何万円にもつくときだからね」
と啓三はなめらかに云ったのだ、
「往復するだけでも二日だぜ、もしものことがあっても一日しきゃ日は取れないのだ、一日だな、一日以上は絶対にだめなんだ、おふくろも罪なときに罪なことをするよ」
ぼくは黙って階段口のほうを見たのだ。
啓三は母を責めているのではない、一時間が何万円にもつくという、自分の言葉の裏書きをしているにすぎないのなのだ。
「こんなところに立っていてもしようがないのだ」
と啓三が云ったのだ、
「中へはいって坐ろうじゃないのだ」
「室町がまだ来ないのだ」
```

小説の登場人物を混乱させるフィルタが完成した。

SSTとつなぎこむ時が来たら[discord.js でボイスチャットを音声認識してテキストログを取る](https://qiita.com/atticatticattic/items/c9e7d5248fda563d68b5)などのありがたい記事を参考にしてやる。

# Zunda-Filterを考える

## ずんだもんを学ぶ

ずんだもんフィルタなのだからずんだもんの学習は欠かせない。ずんだもんのYouTubeを片っ端からみて「ずんだもんっぽさ」を理解する。

参考とした動画

+ [【漫才解説】ずんだもんとちょっと学ぶ「日本銀行の闇」](https://youtu.be/ljcnFE4tA6A)
+ [【漫才解説】ずんだもんと学ぶ「ブラック企業」](https://youtu.be/Hct3TFWgdRw)
+ [【漫才解説】ずんだもんと学ぶ「地球平面説」](https://youtu.be/73tUR81VQHc)

浮かび上がったずんだもんっぽさ

+ 一人称
  + ぼく: 自身
  + ウチ: 所属コミュニティ全体
+ い抜き言葉
+ 助詞飛ばし
  + ex) 必要がないのだ -> 必要ないのだ
+ 相手への呼びかけを行う
  + 文頭に「茜！」と付ける
+ 同意を求める場面では「～しよう！」が入る
  + ex) 一緒に取り戻そう！
+ 「ゆく」ではなく「いく」
+ 体言止め
+ 敬語を使わない
+ 語尾
  + 「～ってこと」
  + 「なのだ」
    + 語尾の「のだ」は疑問形でも発生
      + ex) 知っているのだ？
  + 「～てもいいよ！」
    + 「～してもよい」で切れるのではなく、助詞の「よ」が付く

動画でよく見かけた特徴は3つあり、それぞれ対応が必要

| 特徴 | 対応 |
| :----- | :----- |
| 敬語を使わない | 敬語の解除 |
| 語尾の「のだ」| ムードの変更 |
| 一人称の「ぼく」 | 一人称の変更 |

なので下記コンバータを通せば、ずんだもんっぽさを上げる事ができそうだ。

```mermaid
sequenceDiagram;

participant sst as SST
participant converter1 as unti-honorific-converter
participant converter2 as moode-converter
participant converter3 as first-person-converter
actor buka as 俺

sst ->> converter1: 文
converter1 ->> converter1: 敬語の解除
converter1 ->> converter2: 非敬語文
converter2 ->> converter2: ムードの変更
converter2 ->> converter3: 非敬語&説明ムードの文
converter3 ->> converter3: 一人称変更
converter3 ->> buka: ずんだもん化された文
```

## 敬語の解除

まずは敬語についておさらいする。2007年に[敬語の指針](https://www.bunka.go.jp/seisaku/bunkashingikai/kokugo/hokoku/pdf/keigo_tosin.pdf)が発表され、分類が3つから5つに変わっている。

| before 2007 | after 2007 | 概要 |
| :----- | :------ | :----- |
| 丁寧語 | 丁寧語 | ですますを付ける。「ここは危険です」とか |
| 丁寧語 | 美化語 | 「お」を付ける。「お花」とか。 |
| 謙譲語 | 謙譲語Ⅰ | 「お～する」「お～いただく」。「お越しいただく」とか |
| 謙譲語 | 謙譲語Ⅱ(丁重語) | 「～いたす」や「弊～」とか付ける。「弊社で対処いたします」とか |
| 尊敬語 | 尊敬語| 「お～となる」。「お立ちになる」とか |

単純に「のだ」(or「なのだ」)を付けて違和感があるものだけを対処する。

| 敬語分類 | 原文 | ずんだ化 | 結果 |
| :---- | :---- | :---- | :----- |
| 丁寧語 | ここは危険です | ここは危険ですなのだ | NG |
| 美化語 | お花だ | お花なのだ | OK(*1) |
| 謙譲語Ⅰ | お越しいただく | お越しいただくのだ | OK |
| 謙譲語Ⅱ(丁重語) | 弊社で対処いたします | 弊社で対処いたしますなのだ | OK(*2) |
| 尊敬語| お立ちになる | お立ちになるのだ | OK |

+ *1: 「だ」の変換はムードにて行う為、OKとする
+ *2: これぐらいの違和感は良いって思った。

上記より、丁寧語の「です・ます」を対象とする。

「です・ます」の解除は下記で置き換える

| 直前 | 変換規則 | 例文 |
| :----- | :----- | :----- |
| 名詞 | 「です・ます」の削除 | ここが姫路です -> ここが姫路 |
| 形容詞  | 「です・ます」の削除。 | 姫路は美しいです -> 姫路は美しい |
| 動詞  | 「です・ます」の削除 + 活用に応じた語尾変化(連用 -> 基本)。 | ここに泊まります -> ここに泊まる |
| 形容動詞 | 「です」 -> 「な」の変換 | 静かですので -> 静かなので |
| 助詞 - て | 「です・ます」を「る」へ変換(複合語) | 載せてます -> 載せてる |
| 助詞 - その他 | 「です・ます」の削除 | そこからです -> そこから |
| 接続詞 - ですが | 「だけど」へ変換 | ですが部長 -> だけど部長 |

否定の「ません」対応は連用形から未然形への変化とする。

| 直前 | 変換規則 | 例文 |
| :----- | :----- | :----- |
| 動詞 - 五段活用 | 動詞活用形変化 +「ません」->「ない」変換 | あなたとは話しません -> あなたとは話さない |
| 動詞 - 上一段活用 | 動詞活用形変化 +「ません」->「ない」変換 | ここでは降りません -> ここでは降りない |
| 動詞 - 下一段活用 | 動詞活用形変化 +「ません」->「ない」変換 | ごみは集めません -> ごみは集めない |
| 動詞 - カ行変格活用 | 動詞活用形変化 +「ません」->「ない」変換 | 明日は来ません -> 明日は来ない |
| 動詞 - サ行変格活用 | 「ません」->「ない」変換 | 私はしません -> 私はしない |

「～しよう」の丁寧な言い方も対応する

| 直前 | 変換規則 | 例文 |
| :----- | :----- | :----- |
| 動詞 - 五段活用 | 連用形変化 +「ましょう」 -> 「う」変換 | 話しましょう -> 話そう |
| 動詞 - 上一段活用 | 未然形ウ接続変化 +「ましょう」 -> 「う」変換  | 降りましょう -> 降りよう |
| 動詞 - 下一段活用 | 未然形ウ接続変化 +「ましょう」 -> 「よう」変換  | 集めましょう -> 集めよう |
| 動詞 - カ行変格活用 | 未然形ウ接続変化 +「ましょう」 -> 「う」変換 | 明日は来ません -> 明日は来ない |
| 動詞 - サ行変格活用 | 未然形ウ接続変化 +「ましょう」 -> 「う」変換 | 私はしません -> 私はしない |

過去形の対応

| 直前 | 変換規則 | 例文 |
| :----- | :----- | :----- |
| 名詞 | 「でした」 -> 「だった」 | 体調不良でした -> 体調不良だった |
| 動詞 | 「まし」の削除 | 昨日話しました -> 昨日話した |

音便の対応

| 直前 | 変換規則 | 例文 |
| :----- | :----- | :----- |
| イ音便変化 | 「まし」の削除 + 「いた」変化 | さっき書きました -> さっき書いた |
| 促音便変化 | 「まし」の削除 + 「った」変化 | 手を切りました -> 手を切った |
| 撥音便変化 | 「まし」の削除 + 「んだ」変化 | 昨日読みました -> 昨日読んだ |

連続する敬語の対応

| 直前 | 変換規則 | 例文 |
| :------| :------ | :----- |
| 動詞 | 未然形変化 +「ませんでした」-> 「なかった」| 彼は来ませんでした -> 彼は来なかった |

下記の様な特殊なケースはもはやどうなるのが正しいのか分からんので対応できそうならやる(やらない)

```text
ですです。そうです。 -> 謎
```

## 「～のだ」を理解する

[日本人のための日本語文法](https://www.amazon.co.jp/%E6%97%A5%E6%9C%AC%E4%BA%BA%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E6%96%87%E6%B3%95%E5%85%A5%E9%96%80-%E8%AC%9B%E8%AB%87%E7%A4%BE%E7%8F%BE%E4%BB%A3%E6%96%B0%E6%9B%B8-%E5%8E%9F%E6%B2%A2%E4%BC%8A%E9%83%BD%E5%A4%AB-ebook/dp/B01M8GUXYE/ref=sr_1_1?adgrpid=52895452283&gclid=CjwKCAiA2L-dBhACEiwAu8Q9YJZNeGz0tiHsfpO_FiJHSunDOOaRqAxh7jGPtbLi8rUCdREvXNGE7RoCAikQAvD_BwE&hvadid=618632176741&hvdev=c&hvlocphy=9053260&hvnetw=g&hvqmt=e&hvrand=18149900470348561193&hvtargid=kwd-334079031728&hydadcr=3634_13606498&jp-ad-ap=0&keywords=%E6%97%A5%E6%9C%AC%E4%BA%BA%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E6%96%87%E6%B3%95&qid=1672524041&sr=8-1)によると、「～のだ」は日本語文法における説明のムード(別「モダリティ」)と呼ばれ、一般的な会話では「～んだ/～んです」で変換されるものとの事。「こまけぇこたぁいいんだよ」って感じ。尚、参考にさせてもらった動画で数えたのだが、ある動画の内ずんだもんが発した文の58%が説明のムードに該当した。これだけ高けりゃ片っ端から説明のムードに変換しても良いと判断した(適当)。

ムードは深そうなのでざっと本で読んで理解できる分だけを変換対象とする。対象とするムードは下記17。

| 対 | ムード | 語尾例 | 例文 |
| :----- | :----- | :---- | :----- |
| 事 | 断定 | φ | 僕は腹筋できる人だφ |
| 事 | 意志 | φ | 僕は腹筋するφ |
| 事 | 推量 | らしい | 僕は腹筋するらしい |
| 事 | 確信 | はずだ | 僕は腹筋するはずだ |
| 事 | 説明 | のだ | 僕は腹筋できるのだ |
| 事 | 非断定 | と思う | 僕は腹筋できると思う |
| 事 | 可能性 | かもしれない | 僕は腹筋できるかもしれない |
| 事 | 意志 | つもりだ | 僕は腹筋するつもりだ |
| 事 | 願望 | したい | 僕は腹筋したい |
| 人 | 勧誘 | しましょう | 一緒に腹筋しましょう |
| 人 | 依頼 | てください | 一緒に腹筋してください |
| 人 | 命令 | なさい | 一緒に腹筋しなさい |
| 人 | 許可 | てもよい | 一緒に腹筋してもよい |
| 人 | 禁止 | てはいけない | 一緒に腹筋してはいけない |
| 人 | 質問 | か | 一緒に腹筋したか？ |
| 人 | 確認 | だろう | 昨日、一緒に腹筋しただろう |
| 人 | 同意 | ね | 一緒に腹筋したいね |

ムードを変換する際、単純に「のだ」を付けてしまうと日本語としておかしくなる

| 対 | ムード | 例文末尾に「のだ」 | 結果 | 理想 |
| :----- | :---- | :----- | :----- | :----- |
| 事 | 断定 | 僕は腹筋できる人だのだ | 変 | 僕は腹筋できる人なのだ |
| 事 | 意志 | 僕は腹筋するのだ | OK | |
| 事 | 推量 | 僕は腹筋するらしいのだ | OK | |
| 事 | 確信 | 僕は腹筋するはずだのだ | 変 | 僕は腹筋するはずなのだ |
| 事 | 説明 | 僕は腹筋できるのだのだ | 変 | 僕は腹筋できるのなのだ |
| 事 | 非断定 | 僕は腹筋できると思うのだ | OK | |
| 事 | 可能性 | 僕は腹筋できるかもしれないのだ | OK | |
| 事 | 意志 | 僕は腹筋するつもりだのだ | 変 | 僕は腹筋するつもりなのだ |
| 事 | 願望 | 僕は腹筋したいのだ | OK | |
| 人 | 勧誘 | 一緒に腹筋しましょうのだ | 変 | 一緒に腹筋しましょうなのだ |
| 人 | 依頼 | 一緒に腹筋してくださいのだ | 変 | 一緒に腹筋してくださいなのだ |
| 人 | 命令 | 一緒に腹筋しなさいのだ | 変 | 一緒に腹筋しなさいなのだ |
| 人 | 許可 | 一緒に腹筋してもよいのだ | OK | |
| 人 | 禁止 | 一緒に腹筋してはいけないのだ | OK | |
| 人 | 質問 | 一緒に腹筋したかのだ？ | 変 | 一緒に腹筋したのだ？ |
| 人 | 確認 | 昨日、一緒に腹筋しただろうのだ | 変 | 昨日、一緒に腹筋しただろうなのだ |
| 人 | 同意 | 一緒に腹筋したいねのだ | 変 | 一緒に腹筋したいね |

結果として変換規則は下記の通りとなる

| 対 | ムード | 語尾例 | 規則 |
| :----- | :----- | :---- | :----- |
| 事 | 断定 | φ | だ -> なのだ |
| 事 | 意志 | φ | +のだ |
| 事 | 推量 | らしい | +のだ |
| 事 | 確信 | はずだ | だ -> なのだ |
| 事 | 説明 | のだ | 変換無し |
| 事 | 非断定 | と思う | +のだ |
| 事 | 可能性 | かもしれない | +のだ |
| 事 | 意志 | つもりだ | だ -> なのだ |
| 事 | 願望 | したい | +のだ |
| 人 | 勧誘 | しましょう | +なのだ |
| 人 | 依頼 | てください | +なのだ |
| 人 | 命令 | なさい | +なのだ |
| 人 | 許可 | てもよい | +のだ |
| 人 | 禁止 | てはいけない | +のだ |
| 人 | 質問 | か | か -> のだ |
| 人 | 確認 | だろう | +なのだ |
| 人 | 同意 | ね | 変換無し |

## 一人称を変換する

ずんだもんの一人称は「ぼく」。なので、Mecabを利用して一人称を判断&置き換える。「私達」のような複数人は変換しない(めんどくさくなった)。
特に深い考察とかいらないと判断したので適当な絵でも乗せとく。

<div align="center">
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/f9ee3b6c-a08e-31ef-fe81-30f557793c88.png">
</div>
<div style="text-align: center;">
<font color="dark_pink">あけましておめでとうございます！(2分で書いた)</font>
</div>

# Zunda-Filterを実装する

今回は泥臭くフィルタを実装する。最低限の決めごとは2点だけ。

+ コマンドで利用する
+ MeCab使う

## 準備

とりあえずGoで組むので準備。開発環境はDebian 11。

```shell
sudo apt update
sudo apt upgrade -y
sudo apt install -y \
  build-essential \
  sqlite3
# mecab install
mecab_version="0.996"
curl -fsSL 'https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7cENtOXlicTFaRUE' -o mecab-${mecab_version}.tar.gz
tar zxfv mecab-${mecab_version}.tar.gz
cd mecab-${mecab_version}
./configure --with-charset=utf8
make
make check
sudo make install
sudo ldconfig
# mecab dictionary install
mecab_dictionary_version="2.7.0-20070801"
curl -fsSL 'https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7MWVlSDBCSXZMTXM' -o mecab-ipadic-${mecab_dictionary_version}.tar.gz
tar zxfv mecab-ipadic-${mecab_dictionary_version}.tar.gz
cd mecab-ipadic-${mecab_dictionary_version}
./configure --with-charset=utf8
make
sudo make install
# go install
curl -OL https://go.dev/dl/go1.19.4.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.19.4.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
echo 'export PATH=$PATH:/usr/local/go/bin' >> $HOME/.profile
```

goのインストールまでできたらプロジェクトを作成する

```shell
mkdir zunda-filter
cd zunda-filter
go mod init github.com/miyatama/zunda-filter
# go mecabの対処
# export CGO_LDFLAGS="-L/usr/local/lib -lmecab -lstdc++"
# export CGO_CFLAGS="-I/usr/local/include"
export CGO_LDFLAGS="`mecab-config --libs`"
export CGO_CFLAGS="-I`mecab-config --inc-dir`"
go get github.com/bluele/mecab-golang
```

プロジェクトの構成は下記の通り

| dir | description |
| :----- | :----- |
| cmd | コマンド本体 |
| bin | ビルド結果 |
| data | ログ用configなど |
| log | ロガー |
| filters | 敬語解除フィルターなど |
| zunda_mecab | Mecabに関連する機能 |

参考リンク

+ [Go installation](https://go.dev/doc/install)
+ [【Golang】Goで形態素解析する - mecab-golang](https://rennnosukesann.hatenablog.com/entry/2020/08/08/000000)
+ [【Linux】Mecabの文字化けを直す](https://qiita.com/DisneyAladdin/items/67d986fdb402f3b1c350)
+ [MeCab Installation](http://taku910.github.io/mecab/#download)
+ [Go言語開発を便利にするMakefileの書き方](https://qiita.com/yoskeoka/items/317a3afab370155b3ae8)
+ [Go で使う Makefile の育て方](https://zenn.dev/rosylilly/articles/202105-go-makefile)

## 敬語の解除

filters/honorific_filter.go

先に決めた変換規則に則って変換を行う(抜粋)

```go
package filters

import (
        "go.uber.org/zap"
        "zundafilter/zunda_mecab"
)

type HonorificFilter struct {
        ZundaDb      ZundaDbController
        MecabWrapper *zunda_mecab.MecabWrapper
        Logger       *zap.Logger
}

type HonorificCondition struct {
        CheckWord         bool
        Word              string
        CheckWordType     bool
        WordType          zunda_mecab.MecabWordType
        CheckOriginalForm bool
        OriginalForm      string
}


func (h *HonorificFilter) Convert(text string) (string, error) {
        defer h.Logger.Sync()
        sugar := h.Logger.Sugar()
        sugar.Debug("HonorificFilter#Convert()") 

        features, err := h.MecabWrapper.ParseToNode(text)
        if err != nil {
                return "", nil
        }

        convertedFeatures := convert(
                features,
                []func([]zunda_mecab.MecabFeature) []zunda_mecab.MecabFeature{
                        h.convertVerbBeforePastHonorificNegative,
                        h.convertSahenVerbBeforePastHorific,
                        h.convertVerbBeforePastHorificHatsuOnbin,
                        h.convertVerbBeforePastHorificIOnbin,
                        h.convertVerbBeforePastHorificSokuOnbin,
                        h.convertNounBeforePastHorific,
                        h.convertVerbBeforeHonorificNegative,
                        h.removePastHonorificWord,
                        h.convertVerbBeforeHonorific,
                        h.convertSpecials,
                        h.removeHonorificWord,
                })

        return h.MecabWrapper.Construct(convertedFeatures), nil

}

func convert(features []zunda_mecab.MecabFeature, converters []func([]zunda_mecab.MecabFeature) []zunda_mecab.MecabFeature) []zunda_mecab.MecabFeature {
        if len(converters) == 0 {
                return features
        }
        return convert(
                converters[0](features),
                converters[1:],
        )
}

/*
* 敬語削除(過去)
 */
func (h *HonorificFilter) removePastHonorificWord(features []zunda_mecab.MecabFeature) []zunda_mecab.MecabFeature {
        defer h.Logger.Sync()
        sugar := h.Logger.Sugar()
        sugar.Debug("removePastHonorificWord()")

        honorificWords := getPastHonorificWords()
        if !hasHonorificWord(features, honorificWords) {
                return features
        }

        index := getHonorificIndex(features, honorificWords)
        sugar.Debugf("removePastHonorificWord() index = %d", index)
        return append(features[:index], features[index+1:]...)
}
```

## ムード変換

filters/mood_filter.go

ムードの判定と変換を行う(抜粋)

```go
package filters

import (
        "fmt"
        "go.uber.org/zap"
        "strings"
        "zundafilter/zunda_mecab"
)

type MoodFilter struct {
        MecabWrapper *zunda_mecab.MecabWrapper
        Logger       *zap.Logger
}
type MoodConvertResult struct {
        Features []zunda_mecab.MecabFeature
        Parsed   bool
}

func (m *MoodFilter) Convert(text string) (string, error) {
        defer m.Logger.Sync()
        sugar := m.Logger.Sugar()

        features, err := m.MecabWrapper.ParseToNode(text)
        if err != nil {
                return "", nil
        }

        // パース結果の出力
        for _, feature := range features {
                sugar.Debug(feature.String())
        }

        converters := []func([]zunda_mecab.MecabFeature) MoodConvertResult{
                m.convertNaiAdjectiveMood,
                m.convertPastMood,
                m.convertProhibitionMood,
                m.convertDesireMood,
                m.convertAllowMood,
                m.convertConclusionConversationMood,
                m.convertInvitationMood,
                m.convertOrderTaigenMood,
                m.convertOrderMood,
                m.convertConfirmationMood,
                m.convertQuestionIntentionMood,
                m.convertQuestionMood,
                m.convertUndecisionMood,
                m.convertAgreementMood,
                m.convertRequestMood,
                m.convertConfidenceMood,
                m.convertIntention2Mood,
                m.convertIntentionMood,
                m.convertGuessMood,
                m.convertPossibilityMood,
                m.convertAffirmativeMood,
        }
        for _, converter := range converters {
                MoodConvertResult := converter(features)
                if MoodConvertResult.Parsed {
                        return m.MecabWrapper.Construct(MoodConvertResult.Features), nil
                }
        }

        return text, nil

}

/*
* 断定-口頭のムード
* 条件: んだ
* ex) 僕は腹筋できると思う
 */
func (m *MoodFilter) convertConclusionConversationMood(features []zunda_mecab.MecabFeature) MoodConvertResult {
        defer m.Logger.Sync()
        sugar := m.Logger.Sugar()
        sugar.Debug("convertConclusionConversationMood()")

        conditions := []zunda_mecab.MecabCondition{
                {
                        ConditionType: zunda_mecab.MecabConditionTypeOne,
                        Features: []zunda_mecab.MecabConditionFeature{

                                {
                                        CheckWord:            true,
                                        Word:                 "ん",
                                        CheckWordType:        true,
                                        WordType:             zunda_mecab.MecabWordTypeNoun,
                                        CheckOriginalForm:    true,
                                        OriginalForm:         "ん",
                                        CheckConjugationForm: false,
                                        ConjugationForm:      zunda_mecab.MecabConjugationFormNone,
                                },
                        },
                },
                {
                        ConditionType: zunda_mecab.MecabConditionTypeNothingOrContinue,
                        Features: []zunda_mecab.MecabConditionFeature{

                                {
                                        CheckWord:            true,
                                        Word:                 "だ",
                                        CheckWordType:        true,
                                        WordType:             zunda_mecab.MecabWordTypeAuxiliaryVerb,
                                        CheckOriginalForm:    true,
                                        OriginalForm:         "だ",
                                        CheckConjugationForm: false,
                                        ConjugationForm:      zunda_mecab.MecabConjugationFormNone,
                                },
                        },
                },
        }

        match, index := m.MecabWrapper.GetMatchIndex(features, conditions)
        if !match {
                sugar.Debug("convertConclusionConversationMood() - not match")
                return MoodConvertResult{Features: features, Parsed: false}
        }

        // ムード後の文字列
        afterTextMatch := (index + 1) < len(features)

        // 記号{0..*}+EOS -> なのだ+記号{0..*}+EOS
        texts := []string{}
        texts = append(texts, m.MecabWrapper.Construct(features[:index]))
        texts = append(texts, "のだ")
        if afterTextMatch {
                texts = append(texts, m.MecabWrapper.Construct(features[index+2:]))
        }
        exchangeFeatures, err := m.MecabWrapper.ParseToNode(strings.Join(texts, ""))
        if err != nil {
                sugar.Errorf("convertConclusionConversationMood() - %v", err)
                return MoodConvertResult{Features: features, Parsed: false}
        }

        sugar.Infof("convertConclusionConversationMood() - converted: %s", m.MecabWrapper.Construct(exchangeFeatures))
        return MoodConvertResult{Features: exchangeFeatures, Parsed: true}
}
```

## 一人称変更

filters/pronoun_filter.go

代名詞を「ぼく」に変えるだけ(抜粋)

```go
package filters

import (
        "go.uber.org/zap"
        "strings"
        "zundafilter/zunda_mecab"
)

type PronounFilter struct {
        MecabWrapper *zunda_mecab.MecabWrapper
        Logger       *zap.Logger
}
type PronounConvertResult struct {
        Features []zunda_mecab.MecabFeature
        Parsed   bool
}

func (m *PronounFilter) Convert(text string) (string, error) {
        defer m.Logger.Sync()
        sugar := m.Logger.Sugar()

        features, err := m.MecabWrapper.ParseToNode(text)
        if err != nil {
                return "", nil
        }

        // パース結果の出力
        for _, feature := range features {
                sugar.Debug(feature.String())
        }

        converters := []func([]zunda_mecab.MecabFeature) PronounConvertResult{
                m.convertPronoun,
        }
        for _, converter := range converters {
                PronounConvertResult := converter(features)
                if PronounConvertResult.Parsed {
                        return m.MecabWrapper.Construct(PronounConvertResult.Features), nil
                }
        }

        return text, nil

}

/*
* 代名詞の変換
* 条件: 代名詞 + 助詞
* ex) 私は野球が好きです
 */
func (m *PronounFilter) convertPronoun(features []zunda_mecab.MecabFeature) PronounConvertResult {
        defer m.Logger.Sync()
        sugar := m.Logger.Sugar()
        sugar.Debug("convertPronoun()")

        conditions := []zunda_mecab.MecabCondition{
                {
                        ConditionType: zunda_mecab.MecabConditionTypeOne,
                        Features: []zunda_mecab.MecabConditionFeature{

                                {
                                        CheckWord:            false,
                                        Word:                 "",
                                        CheckWordType:        true,
                                        WordType:             zunda_mecab.MecabWordTypeNoun,
                                        CheckWordSubType1:    true,
                                        WordSubType1:         zunda_mecab.MecabWordSubType1NounPronoun,
                                        CheckOriginalForm:    false,
                                        OriginalForm:         "",
                                        CheckConjugationForm: false,
                                        ConjugationForm:      zunda_mecab.MecabConjugationFormNone,
                                },
                        },
                },
                {
                        ConditionType: zunda_mecab.MecabConditionTypeOne,
                        Features: []zunda_mecab.MecabConditionFeature{

                                {
                                        CheckWord:            false,
                                        Word:                 "",
                                        CheckWordType:        true,
                                        WordType:             zunda_mecab.MecabWordTypeParticle,
                                        CheckWordSubType1:    false,
                                        WordSubType1:         zunda_mecab.MecabWordSubType1None,
                                        CheckOriginalForm:    false,
                                        OriginalForm:         "",
                                        CheckConjugationForm: false,
                                        ConjugationForm:      zunda_mecab.MecabConjugationFormNone,
                                },
                        },
                },
        }

        match, conditionIndex := m.MecabWrapper.GetMatchIndex(features, conditions)
        if !match {
                sugar.Debug("convertPronoun() - not match")
                return PronounConvertResult{Features: features, Parsed: false}
        }

        // ムード後の文字列
        afterTextMatch := (conditionIndex + 1) < len(features)

        // 代名詞 + 助詞 -> ぼく + 助詞
        texts := []string{}
        texts = append(texts, m.MecabWrapper.Construct(features[:conditionIndex]))
        texts = append(texts, "ぼく")
        if afterTextMatch {
                texts = append(texts, m.MecabWrapper.Construct(features[conditionIndex + 1:]))
        }
        exchangeFeatures, err := m.MecabWrapper.ParseToNode(strings.Join(texts, ""))
        if err != nil {
                sugar.Errorf("convertPronoun() - %v", err)
                return PronounConvertResult{Features: features, Parsed: false}
        }

        sugar.Infof("convertPronoun() - converted: %s", m.MecabWrapper.Construct(exchangeFeatures))
        return PronounConvertResult{Features: exchangeFeatures, Parsed: true}
}
```

## 挙動確認

Makefileとかを適当に揃えてコマンドを叩く

```shell
make clean
make test
make build
echo "継ぎます" | ./bin/zundafilter
継ぐのだ
```

# ふりかえり

ムードの判定を機械学習に任せようかと思ったが、~~データ作るのがめんどくさかった~~学習させるより自分で地道に実装した方が日本語の勉強になると思ってやめた。ある程度データがたまったら勉強がてら機械学習にぶち込んでみる。今回の収穫は日本語の難しさ。

+ 敬語の深さ:状況(自分、相手、第三者、場、主題、内容、口頭/文書、etc)によって何が正しいのかが変わってくる。いくら文章として敬語的でも内容がおかしければ敬語としては適当でない。
+ 「は」の貫通力
+ 2つの受身(直接・間接)
+ 自動詞/他動詞/使役/受身
+ 学校文法と日本語文法の違い

中でも「は」の貫通力はすさまじい。

```text
私は都内に住む48才会社員。いつものように満員電車で出社し、満員電車で帰宅する。
いつも通りの帰り道で猫を拾った。ガリガリで痩せており、家で猫缶を出すとがっつ
いて食べた。猫が猫缶に夢中になっている横で食パンをかじりながらテレビを付けた。
```

この文で猫を拾ったのは「私」で、テレビを付けたのも「私」。ガリガリで痩せているのは「猫」。「私」は先頭からテレビをつけるまで貫通。ガリガリも華麗に回避。すごい。
学生の頃に「日本から出ないから文法とかどうでもいい」とか思ってた自分をぶん殴りたい。

流行りの技術を触るのも楽しいけど、参考図書を探しまわる実装もまた別の楽しみがあった(時間は掛かる)。
初手でChatGPTに頼ろうとしてなんだけど、趣味開発なんて回り道してなんぼだと思う。

<div align="center">
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/c4dcc749-2404-5c59-3c9b-2520b49c0af9.png"></img>
</div>
<div style="text-align: center;">
<font color="dark_pink">ChatGPT様に頼ろうとしたら自分で組めって言われた時のログ</font>
</div>

# 参考図書

+ 日本人のための日本語文法入門 - 原沢伊都夫
+ 神字日文考 - 吉田信啓
+ 敬語表現 - 坂本恵/川口義一/蒲谷宏
+ 言語学抗議 - その起源と未来 - 加藤重広