M5Stack Module-LLMとCardKBで日本語入力（の入口）

# やりたいこと

Module-LLMに対して日本語文字列でプロンプトを投げるとちゃんと日本語で返ってくるので、CardKBで日本語をポチポチと入力してModule-LLMと日本語でやりとりしたい。本当はかな漢字変換までやりたいのだけれど、形態素解析とか始めると大変なのでひらがなへの変換のみ。

# 材料

- Module-LLM
- CoreS3
- CardKB

https://www.switch-science.com/products/8445

## やってること

- CardKBからの入力をreadしてEnterキーの入力時点でローマ字→ひらがなの変換を行い、Module-LLMへのプロンプトとして投げる
- 一応ローマ字入力時のDeleteキーも応答しますが、表示は書き換えてないのでちょっとおかしい。入力バッファは更新されている、はず
- 大元のソースは、SerialTextAssistant。シリアルモニタからの入力をCardKBのハンドリングとひらがな変換に置き換え

https://github.com/m5stack/M5Module-LLM/tree/main/examples/SerialTextAssistant

### ローマ字かな変換

ローマ字っていろんなパターンがあってめんどい。ひと通り変換できそうだけれど、まだ足りないかも（今気がついたけれど「しぇ」とかない）。

```c:convertHiraganaTable.h
#ifndef _CONVERT_HIRAGANA_TABLE_H_
#define _CONVERT_HIRAGANA_TABLE_H_

typedef struct {
  char *romaji;
  char *hiragana;
} RomajiKanaMap;

RomajiKanaMap table[] = {
  {"a", "あ"}, {"i", "い"}, {"u", "う"}, {"e", "え"}, {"o", "お"},
  {"ka", "か"}, {"ki", "き"}, {"ku", "く"}, {"ke", "け"}, {"ko", "こ"},
  {"ga", "が"}, {"gi", "ぎ"}, {"gu", "ぐ"}, {"ge", "げ"}, {"go", "ご"},
  {"sa", "さ"}, {"si", "し"}, {"su", "す"}, {"se", "せ"}, {"so", "そ"},
  {"shi", "し"}, 
  {"za", "ざ"}, {"zi", "じ"}, {"zu", "ず"}, {"ze", "ぜ"}, {"zo", "ぞ"},
  {"ji", "じ"}, 
  {"ta", "た"}, {"chi", "ち"}, {"tsu", "つ"}, {"te", "て"}, {"to", "と"},
  {"ti", "ち"}, {"tu", "つ"}, 
  {"da", "だ"}, {"di", "ぢ"}, {"du", "づ"}, {"de", "で"}, {"do", "ど"},
  {"na", "な"}, {"ni", "に"}, {"nu", "ぬ"}, {"ne", "ね"}, {"no", "の"},
  {"ha", "は"}, {"hi", "ひ"}, {"fu", "ふ"}, {"he", "へ"}, {"ho", "ほ"},
  {"ba", "ば"}, {"bi", "び"}, {"bu", "ぶ"}, {"be", "べ"}, {"bo", "ぼ"},
  {"pa", "ぱ"}, {"pi", "ぴ"}, {"pu", "ぷ"}, {"pe", "ぺ"}, {"po", "ぽ"},
  {"ma", "ま"}, {"mi", "み"}, {"mu", "む"}, {"me", "め"}, {"mo", "も"},
  {"ya", "や"}, {"yu", "ゆ"}, {"yo", "よ"},
  {"ra", "ら"}, {"ri", "り"}, {"ru", "る"}, {"re", "れ"}, {"ro", "ろ"},
  {"wa", "わ"}, {"wo", "を"}, {"nn", "ん"},
  {"xya", "ょ"}, {"xyu", "ゅ"}, {"xyo", "ょ"}, {"xtu", "っ"},
  {"kya", "きゃ"}, {"kyu", "きゅ"}, {"kyo", "きょ"},
  {"gya", "ぎゃ"}, {"gyu", "ぎゅ"}, {"gyo", "ぎょ"},
  {"sha", "しゃ"}, {"shu", "しゅ"}, {"sho", "しょ"},
  {"ja", "じゃ"}, {"ju", "じゅ"}, {"jo", "じょ"},
  {"cha", "ちゃ"}, {"chu", "ちゅ"}, {"cho", "ちょ"},
  {"dya", "ぢゃ"}, {"dyu", "ぢゅ"}, {"dyo", "ぢょ"},
  {"nya", "にゃ"}, {"nyu", "にゅ"}, {"nyo", "にょ"},
  {"hya", "ひゃ"}, {"hyu", "ひゅ"}, {"hyo", "ひょ"},
  {"bya", "びゃ"}, {"byu", "びゅ"}, {"byo", "びょ"},
  {"pya", "ぴゃ"}, {"pyu", "ぴゅ"}, {"pyo", "ぴょ"},
  {"mya", "みゃ"}, {"myu", "みゅ"}, {"myo", "みょ"},
  {"rya", "りゃ"}, {"ryu", "りゅ"}, {"ryo", "りょ"},
  {"kka", "っか"}, {"kki", "っき"}, {"kku", "っく"}, {"kke", "っけ"}, {"kko", "っこ"},
  {"gga", "っが"}, {"ggi", "っぎ"}, {"ggu", "っぐ"}, {"gge", "っげ"}, {"ggo", "っご"},
  {"ssa", "っさ"}, {"sshi", "っし"}, {"ssu", "っす"}, {"sse", "っせ"}, {"sso", "っそ"},
  {"zza", "っざ"}, {"zzi", "っじ"}, {"zzu", "っず"}, {"zze", "っぜ"}, {"zzo", "っぞ"},
  {"tta", "った"}, {"cchi", "っち"}, {"ttsu", "っつ"}, {"tte", "って"}, {"tto", "っと"},
  {"dda", "っだ"}, {"ddi", "っぢ"}, {"ddu", "っづ"}, {"dde", "っで"}, {"ddo", "っど"},
  {"nna", "っな"}, {"nni", "っに"}, {"nnu", "っぬ"}, {"nne", "っね"}, {"nno", "っの"},
  {"hha", "っは"}, {"hhi", "っひ"}, {"ffu", "っふ"}, {"hhe", "っへ"}, {"hho", "っほ"},
  {"bba", "っば"}, {"bbi", "っび"}, {"bbu", "っぶ"}, {"bbe", "っべ"}, {"bbo", "っぼ"},
  {"ppa", "っぱ"}, {"ppi", "っぴ"}, {"ppu", "っぷ"}, {"ppe", "っぺ"}, {"ppo", "っぽ"},
  {"mma", "っま"}, {"mmi", "っみ"}, {"mmu", "っむ"}, {"mme", "っめ"}, {"mmo", "っも"},
  {"yya", "っや"}, {"yyu", "っゆ"}, {"yyo", "っよ"},
  {"rra", "っら"}, {"rri", "っり"}, {"rru", "る"}, {"rre", "れ"}, {"rro", "ろ"},
  {"wwa", "っわ"}, {"wwo", "っを"}, 
  {"kkya", "っきゃ"}, {"kkyu", "っきゅ"}, {"kkyo", "っきょ"},
  {"ggya", "っぎゃ"}, {"ggyu", "っぎゅ"}, {"ggyo", "っぎょ"},
  {"ssha", "っしゃ"}, {"sshu", "っしゅ"}, {"ssho", "っしょ"},
  {"jja", "っじゃ"}, {"jju", "っじゅ"}, {"jjo", "っじょ"},
  {"ccha", "っちゃ"}, {"cchu", "っちゅ"}, {"ccho", "っちょ"},
  {"ddya", "っぢゃ"}, {"ddyu", "っぢゅ"}, {"ddyo", "っぢょ"},
  {"nnya", "っにゃ"}, {"nnyu", "っにゅ"}, {"nnyo", "っにょ"},
  {"hhya", "っひゃ"}, {"hhyu", "っひゅ"}, {"hhyo", "っひょ"},
  {"bbya", "っびゃ"}, {"bbyu", "っびゅ"}, {"bbyo", "っびょ"},
  {"ppya", "っぴゃ"}, {"ppyu", "っぴゅ"}, {"ppyo", "っぴょ"},
  {"mmya", "っみゃ"}, {"mmyu", "っみゅ"}, {"mmyo", "っみょ"},
  {"rrya", "っりゃ"}, {"rryu", "っりゅ"}, {"rryo", "っりょ"},
  {"-", "ー"}, {".", "。"}, {",", "、"}
};

#define TABLE_SIZE (sizeof(table) / sizeof(table[0]))

void convertRoma2Hiragana(const char *input, char *output) {
  int i, j, len;
  while (*input) {
    int found = 0;
    for (i = 0; i < TABLE_SIZE; i++) {
      len = strlen(table[i].romaji);
      if (strncmp(input, table[i].romaji, len) == 0) {
        strcat(output, table[i].hiragana);
        input += len;
        found = 1;
        break;
      }
    }
    if (!found) {
        strncat(output, input, 1);
        input++;
    }
  }
}

#endif _CONVERT_HIRAGANA_TABLE_H_
```

### メインループとキーボードハンドリング

```c:KeyboardAssistant.ino
#include <Wire.h>
#include <Arduino.h>
#include <M5Unified.h>
#include <M5ModuleLLM.h>
#include "convertHiraganaTable.h"

M5ModuleLLM module_llm;
String llm_work_id;
String received_question;
String input_question;
bool question_ok;

void setup() {
  // put your setup code here, to run once:
  M5.begin();
  Wire.begin();

  M5.Display.clear();
  M5.Display.setTextFont(0);
  M5.Display.setTextSize(1);
  M5.Display.setFont(&fonts::efontJA_12);
  M5.Display.setTextScroll(true);

  // init input string
  input_question = "";

  // set up
  // Serial2.begin(115200, SERIAL_8N1, 16, 17);  // Basic
  // Serial2.begin(115200, SERIAL_8N1, 13, 14);  // Core2
  Serial2.begin(115200, SERIAL_8N1, 18, 17);  // CoreS3

  // llm start
  module_llm.begin(&Serial2);
  M5.Display.printf(">> Check ModuleLLM connection..\n");
  while (1) {
    if (module_llm.checkConnection()) {
      break;
    }
  }

  M5.Display.printf(">> Reset ModuleLLM..\n");
  module_llm.sys.reset();

  M5.Display.printf(">> Setup llm..\n");
  m5_module_llm::ApiLlmSetupConfig_t llm_config;
  llm_config.max_token_len = 1023;
  llm_work_id              = module_llm.llm.setup(llm_config);

  M5.Display.printf(">> Setup finish\n");
  M5.Display.printf(">> Try type your question via CardKB\n\n");
}

void loop() {
  // put your main code here, to run repeatedly:
  M5.update();
  Wire.requestFrom(0x5F, 1);

  question_ok = false;
  while (Wire.available()) {
    // handle keyboard input
    question_ok = handleKeyboard();
    if (question_ok) {
      break;
    }
  }

  if (question_ok) {
    // send question string to llm
    M5.Display.setTextColor(TFT_GREEN);
    M5.Display.printf("\n<< %s\n", received_question.c_str());
    M5.Display.setTextColor(TFT_YELLOW);
    M5.Display.printf(">> ");

    module_llm.llm.inferenceAndWaitResult(llm_work_id, received_question.c_str(), [](String& result) {
        M5.Display.printf("%s", result.c_str());
    });

    /* Clear for next question */
    received_question.clear();

    M5.Display.println();
  }

  delay(20);
}

bool handleKeyboard() {
  bool result = false;
  char c_key;
  char c = Wire.read();
  if (c != 0) {
    switch(c) {
      case 0x0d:
        // enter key
        received_question = henkan(input_question);
        input_question.clear();
        input_question = "";
        c_key = 0x00;
        result = true;
        break;
      case 0x08:
        // delete key
        input_question.remove(input_question.length() - 1);
        c_key = 0x00;
        result = false;
        break;
      default:
        c_key = c;
        result = false;
        break;
    }
    if (c_key != 0x00) {
      input_question += c_key;
      M5.Display.setTextColor(TFT_WHITE);
      M5.Display.printf("%c", c_key);
    }
  }

  return result;
}

String henkan(String input) {
  const char *input_str = input.c_str();
  char output_str[500] = "";

  convertRoma2Hiragana(input_str, output_str);

  return String(output_str);
}
```

# 終わりに

とりあえずは日本語で対話できるようにはなったけれど、全部ひらがななのでLLMとしてもちょっと何言ってるのかわからない応答を返す。やっぱりかな漢字変換が必要ですね。