M5Stack Module-LLMとCardKBとYahoo!かな漢字変換APIで日本語でやりとりする

# やりたいこと

Module-LLMとCardKBで入力して**日本語で**会話したい。
前回こちらの記事でローマ字ひらがな変換をローカルで実装してみたけれど、ひらがなだけだと、応答結果が塩なんですよね。

https://qiita.com/kowloon/items/23e69d8fd8ef0f7e0a1b

というわけで、かな漢字変換をしてみる。

# 材料

- Module-LLM
- M5Stack CoreS3
- CardKB
- Yahoo!かな漢字変換API

https://developer.yahoo.co.jp/webapi/jlp/jim/v2/conversion.html

# Yahoo!かな漢字変換API

- POSTでJSONリクエストすると変換結果が返ってくるAPI
- アプリケーションIDを登録すれば、RESTでリクエスト可能
- resultsの値はレスポンスされる候補の数。とりあえず第一候補だけでいいので1を指定する

```json:リクエストJSON
{
  "id": "1234",
  "jsonrpc": "2.0",
  "method": "jlp.jimservice.conversion",
  "params": {
    "q": "きしゃのきしゃがきしゃできしゃした。",
    "format": "hiragana",
    "mode": "kanakanji",
    "option": ["hiragana", "katakana", "alphanumeric", "half_katakana", "half_alphanumeric"],
    "dictionary": ["base", "name", "place", "zip", "symbol"],
    "results": 1
  }
}
```

レスポンス見ると、resultの中の[segment][candidate][0]の文字列を結合すればとりあえず第一候補で変換結果が得られそう。

```json:レスポンスJSON
{
    "id": "1234",
    "jsonrpc": "2.0",
    "result": {
        "segment": [
            {
                "alphanumeric": "ｋｉｓｈａｎｏ",
                "candidate": [
                    "貴社の"
                ],
                "half_alphanumeric": "kishano",
                "half_katakana": "ｷｼｬﾉ",
                "hiragana": "きしゃの",
                "katakana": "キシャノ",
                "reading": "きしゃの"
            },
            {
                "alphanumeric": "ｋｉｓｈａｇａ",
                "candidate": [
                    "記者が"
                ],
                "half_alphanumeric": "kishaga",
                "half_katakana": "ｷｼｬｶﾞ",
                "hiragana": "きしゃが",
                "katakana": "キシャガ",
                "reading": "きしゃが"
            },
            {
                "alphanumeric": "ｋｉｓｈａｄｅ",
                "candidate": [
                    "汽車で"
                ],
                "half_alphanumeric": "kishade",
                "half_katakana": "ｷｼｬﾃﾞ",
                "hiragana": "きしゃで",
                "katakana": "キシャデ",
                "reading": "きしゃで"
            },
            {
                "alphanumeric": "ｋｉｓｈａｓｈｉｔａ．",
                "candidate": [
                    "帰社した。"
                ],
                "half_alphanumeric": "kishashita.",
                "half_katakana": "ｷｼｬｼﾀ。",
                "hiragana": "きしゃした。",
                "katakana": "キシャシタ。",
                "reading": "きしゃした。"
            }
        ]
    }
}
```

# というわけで

https://qiita.com/kowloon/items/23e69d8fd8ef0f7e0a1b

これに肉付けしていく。

```c:convertKanji.h
#ifndef _CONVERT_KANJI_H_
#define _CONVERT_KANJI_H_

const char *ssid = "__ssid__";
const char *password = "__password__";

const char *api_url = "jlp.yahooapis.jp";
const char *api_endpoint = "/JIMService/V2/conversion";
const char *APPID = "__apikey__";  // Yahoo! APIキー

#define MODE_ROMAN      "roman"
#define MODE_PREDICTIVE "predictive"
#define MODE_KANJI      "kanakanji"

#define FORMAT_ROMAN    "roman"
#define FORMAT_HIRAGANA "hiragana"

WiFiClientSecure client;

String parseResponse(String response) {
  const size_t capacity = JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(1) + 100;
  DynamicJsonDocument doc(capacity);

  int jsonStart = response.indexOf("{");
  if (jsonStart == -1) {
    M5.Display.println("Invalid Response");
    return "";
  }
  String jsonResponse = response.substring(jsonStart);
  DeserializationError error = deserializeJson(doc, jsonResponse);
  if (error) {
    M5.Display.println("JSON Parse Failed");
    return "";
  }
  String resultText = "";
  JsonArray segments = doc["result"]["segment"];

  for (JsonObject segment : segments) {
    String candidate = segment["candidate"][0].as<String>();
    resultText += candidate;
  }

  return (resultText);
}

String sendAPIRequest(String text, String mode = MODE_KANJI, String format = FORMAT_HIRAGANA) {
  String jsonRequest = "{\"id\": \"1234\",\"jsonrpc\": \"2.0\",\"method\": \"jlp.jimservice.conversion\",\"params\": {\"q\": \"" + text + "\",\"format\": \"" + format + "\",\"mode\": \"" + mode + "\",\"option\": [\"hiragana\", \"katakana\", \"alphanumeric\", \"half_katakana\", \"half_alphanumeric\"],\"dictionary\": [\"base\", \"name\", \"place\", \"zip\", \"symbol\"],\"results\": 1}}";

  if (!client.connect(api_url, 443)) {
    M5.Display.println("API Connection Failed");
    return "";
  }

  client.println("POST " + String(api_endpoint) + " HTTP/1.1");
  client.println("Host: " + String(api_url));
  client.println("User-Agent: Yahoo AppID: " + String(APPID));
  client.println("Content-Type: application/json");
  client.println("Content-Length: " + String(jsonRequest.length()));
  client.println("Connection: close");
  client.println();
  client.println(jsonRequest);

  String response = "";
  while (client.connected() || client.available()) {
    if (client.available()) {
      response += client.readStringUntil('\n');
    }
  }
  client.stop();

  String result = parseResponse(response);

  return result;
}

void initConvertKanji() {
  client.setInsecure();
}

String convertKanji(String text, String mode = MODE_KANJI, String format = FORMAT_HIRAGANA) {
  return sendAPIRequest(text, mode, format);
}

#endif _CONVERT_KANJI_H_
```

これで入力したひらがなをAPIに送ると「貴社の記者が汽車で帰社した。」に変換されるはず。

```
String result = convertKanji("きしゃのきしゃがきしゃできしゃした。");
```

# おわりに

APIを利用する＝オフラインLLMではなくなっちゃったのだけれど、まあ、可能性ということで。