[C++] 371 ローマ字を英数モードで打った時のリカバリー その3 再変換までキー5回押下で実装 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

かつてのATOKの様に、かなキー2回押下で再変換までできず、以下のように機能を3分割して実装しました。

英字からひらがな変換:かなキー2回押下、AppleScript
ひらがなを範囲選択:英数キー2回押下、 C++自製関数
再変換:F13キー1回押下、AppleScript

最終的にC++だけで実装できるよう、リファクタリングしていきます。

#include <ApplicationServices/ApplicationServices.h>

void pressShiftLeftArrow(int count) {
    // Shiftキーを押す
    CGEventRef shiftDown = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)56, true);
    CGEventPost(kCGHIDEventTap, shiftDown);
    CFRelease(shiftDown);

    // 文字数分だけ左矢印キーを押す
    for (int i = 0; i < count; ++i) {
        CGEventRef leftArrowDown = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)123, true);
        CGEventRef leftArrowUp = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)123, false);
        CGEventSetFlags(leftArrowDown, kCGEventFlagMaskShift);
        CGEventPost(kCGHIDEventTap, leftArrowDown);
        CGEventPost(kCGHIDEventTap, leftArrowUp);
        CFRelease(leftArrowDown);
        CFRelease(leftArrowUp);
        // イベント間にわずかな遅延を挿入
        usleep(10000); // 10ミリ秒待機
    }

    // Shiftキーを離す
    CGEventRef shiftUp = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)56, false);
    CGEventPost(kCGHIDEventTap, shiftUp);
    CFRelease(shiftUp);
}

[C++] 370 ローマ字を英数モードで打った時のリカバリー その2 かなキー連続押下対応

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

前回のコードでは、かなキーを何となく押し、英字を入力して無意識に2回目を押したりすると不要なひらがなが一気に出現します。

これではまずいので1秒以内に2回目を押した時だけひらがな変換するようにしました。

あとはひらがな変換した文字列を範囲選択して再変換するという処理ですが、流石にC++でもハードルは高そうです。

Carbonフレームワークが今も現役なことに少々驚きました。

#include <Carbon/Carbon.h>
#include <iostream>
#include <string>
#include <unordered_map>
#include <chrono>

std::unordered_map<CGKeyCode, std::string> keyCodeMap = {
    {0, "a"}, {11, "b"}, {8, "c"}, {2, "d"}, {14, "e"}, {3, "f"},
    {5, "g"}, {4, "h"}, {34, "i"}, {38, "j"}, {40, "k"}, {37, "l"},
    {46, "m"}, {45, "n"}, {31, "o"}, {35, "p"}, {12, "q"}, {15, "r"},
    {1, "s"}, {17, "t"}, {32, "u"}, {9, "v"}, {13, "w"}, {7, "x"},
    {16, "y"}, {6, "z"}
};

std::string inputBuffer;
int kanaKeyPressCount = 0;
std::chrono::steady_clock::time_point firstKanaKeyPressTime;

CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
    if (type == kCGEventKeyDown) {
        CGKeyCode keyCode = (CGKeyCode)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);

        if (keyCode == 104) { // かなキー(104)が押された場合
            std::cout << "かなキーを押しました" << std::endl;
            auto now = std::chrono::steady_clock::now();

            if (kanaKeyPressCount == 0) {
                firstKanaKeyPressTime = now;
                kanaKeyPressCount = 1;
            } else if (kanaKeyPressCount == 1) {
                auto duration = std::chrono::duration_cast<std::chrono::seconds>(now - firstKanaKeyPressTime).count();
                if (duration <= 1) { // 1秒以内に2回目が押された場合
                    std::string command = "osascript \"LatinToHiragana.applescript\" " + inputBuffer;
                    std::cout << "command: " << command << std::endl;
                    system(command.c_str()); // コマンドを実行

                    inputBuffer.clear();
                    std::cout << "AppleScript実行。Current Bufferをリセット" << std::endl;
                } else {
                    std::cout << "1秒以上経過のため、カウントをリセット" << std::endl;
                }
                kanaKeyPressCount = 0; // カウントをリセット
            }
        } else {
            auto it = keyCodeMap.find(keyCode);
            if (it != keyCodeMap.end()) {
                inputBuffer += it->second;
                std::cout << "Current Buffer: " << inputBuffer << std::endl;
            } else {
                inputBuffer.clear();
                std::cout << "Current Bufferをリセット" << std::endl;
            }
        }
    }

    return event;
}

int main() {
    CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyDown), eventCallback, nullptr);
    if (!eventTap) {
        std::cerr << "Failed to create event tap" << std::endl;
        return 1;
    }

    CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
    CGEventTapEnable(eventTap, true);
    CFRunLoopRun();

    return 0;
}

[C++] 369 ローマ字を英数モードで打った時のリカバリー その1 Swiftから移植 AppleScript

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

Swift版ではアプリ起動時に入力モードが固定されてしまうため、新たに入力モードを設定するコードを追加する必要がありました。しかし、これが切り替え遅延の原因となり最初の入力文字を取りこぼしてしまいます。

Swiftでは限界を感じたのでC++に移植しました。アプリを起動しても入力モードの切り替えは可能です。遅延もありません。

Swiftはアプリ商品の開発にだけ使用するのが無難ですね。非常にセキュアですが、自由度が低すぎます。Javaと似たような感じです。

#include <Carbon/Carbon.h>
#include <iostream>
#include <string>
#include <unordered_map>

// キーボードキーと文字の対応
std::unordered_map<CGKeyCode, std::string> keyCodeMap = {
    {0, "a"}, {11, "b"}, {8, "c"}, {2, "d"}, {14, "e"}, {3, "f"},
    {5, "g"}, {4, "h"}, {34, "i"}, {38, "j"}, {40, "k"}, {37, "l"},
    {46, "m"}, {45, "n"}, {31, "o"}, {35, "p"}, {12, "q"}, {15, "r"},
    {1, "s"}, {17, "t"}, {32, "u"}, {9, "v"}, {13, "w"}, {7, "x"},
    {16, "y"}, {6, "z"}
};

std::string inputBuffer;
int kanaKeyPressCount;

CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
    if (type == kCGEventKeyDown) {
        CGKeyCode keyCode = (CGKeyCode)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);

        // かなキー(104)が2回連続で押された場合の処理
        if (keyCode == 104) {
            std::cout << "かなキーを押しました" << std::endl; 
            kanaKeyPressCount += 1;

            if (kanaKeyPressCount == 2){
                // AppleScriptを実行する
                std::string command = "osascript \"LatinToHiragana.applescript\" " + inputBuffer;
                std::cout << "command: " << command << std::endl; 
                system(command.c_str());
                
                kanaKeyPressCount = 0;
            }
        } else {
            // キーコードに対応する文字がある場合、inputBufferに追加
            auto it = keyCodeMap.find(keyCode);
            if (it != keyCodeMap.end()) {
                inputBuffer += it->second;
                std::cout << "Current Buffer: " << inputBuffer << std::endl;
            } else {
                inputBuffer.clear();
                std::cout << "Current Bufferをリセット" << std::endl;
            }
        }
    }

    return event;
}

int main() {
    // CGEventTapCreateの呼び出しでkCGEventTapOptionDefaultを使用
    CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyDown), eventCallback, nullptr);
    if (!eventTap) {
        std::cerr << "Failed to create event tap" << std::endl;
        return 1;
    }

    CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
    CGEventTapEnable(eventTap, true);
    CFRunLoopRun();

    return 0;
}

[Swift] 71 ローマ字を英数モードで打った時のリカバリー AppleScript

[Mac M2 Pro 12CPU, Sonoma 14.3.1]

ローマ字を英数モードで打ってしまった時に、かなキー2回押しでひらがなに変換するようにしました。SwiftとAppleScriptを組み合わせています。

ローマ字はヘボン式なのが難点です。”し”は”shi、”ち”はchiになります。また反応速度がやや遅く、入力モードを切り替えてすぐに入力すると最初の1文字を取りこぼします。

これでは常用できませんね。2回押しで一気に再変換まで出来るようにしたかったのですが、反応速度と合わせて今後の課題とします。

[C++] 368 ChatAIアプリの製作 その49 ローカルLLMの性能を引き出す OpenAI互換APIサーバ llama.cpp

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

ChatAIアプリのGUI右側CMDボタンでOpenAI互換APIサーバの起動コマンドを生成し、自動的にターミナルを開いて実行するようにしました(AppleScript)。サーバの状況はターミナルからモニタリングします。

CodeLlama日本語版とチャットしてみると相当に気難しくてうまく誘導しないと正答にたどり着けないことが分かりました。これはこれで面白く、ローカルLLMですから無料というのが魅力です。

gpt-4への課金がどれくらい削減できるか、楽しみです。

1回目の回答:”コード修正は無理”と回答
3回目の回答:やり取りを経て何とか正解を引き出せた

[C++] 367 ChatAIアプリの製作 その48 ローカルLLMをOpenAI互換APIサーバ(llama.cpp)で動かす

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

llama.cppでOpenAI互換APIサーバを立ち上げ、ChatGPTのようにチャットできるようにしました。

モデルは ELYZA-japanese-CodeLlama-7b-instruct-q4_K_M.gguf です。

cd /Volumes/DATA_m1/AI/llama.cpp && ./server -m models/ELYZA-japanese-CodeLlama-7b-instruct-q4_K_M.gguf -ngl 1 -c 4096

サーバのURLは以下のようになります。
url : http://localhost:8080/v1/chat/completions

ターミナルでサーバの動作を監視

参考サイト

[AI] ローカルLLM検証アプリの製作 llama.cpp SwiftUI

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]

ローカルLLMの性能を検証するアプリをSwiftUIで製作しました。

modelsディレクトリに入っているggufファイルを選択し、promptを入力してtxtファイル化、llama.cppのmainコマンドを作成します。コマンドはクリップボードに自動コピーされます。あとはターミナルにコマンドを貼り付けて実行し、responseを手動で右側のTextEditorにペーストしてtxtファイルにします。

promptファイルとresponseファイルのプレフィックスはコマンド作成時のタイムスタンプ(yymmdd_hhmmss)になっています。

今回はほとんどclaude-3 opusとgpt-4にコードを考えてもらいました。Buttonラベルの改行表示についてはclaude-3では埒が開かず、gpt-4がテキスト分割を提案して不本意ながら解決しました。SwiftUIに関してはclaude-3とgpt-4はほぼ互角といった印象です。

SwiftUIのmacOS版ではButtonのラベルを改行できないようです。iOS版では問題なく出来るはずですが、macOS版の後進性に若干引いています。

※claude-3利用状況
claude-3のAPIキーを取得して5日経ち、5ドルの無料分がほぼ無くなりました。私の用途ではgpt-4に対する優位性を体感できなかったので、今後は割安なgpt-4メインに戻ります。なおclaude-3 APIの支払いで個人はマイナンバーの入力を求められているため、登録は見送りました。Claude Pro($20/month)への登録もしません。

[C++] 366 ChatAIアプリの製作 その47 “role:systemの扱い”公式版 claude-3 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]
対応LLM:gpt-4, gpt-4-vision, claude-3

claude-3ではgptのようにrole:systemは設定できず、systemという独立したパラメータとして扱います。つまりmodelやtemperatureと同じです。

以前の記事で紹介したようにuserのcontentの先頭に追加しても効果は同じだと思いますが、一応公式の方法に従います。

if (urls == ""){
    // claude-3 画像なし
    requestData = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"user\",\"content\":\"" + question + "\"}], \"system\":\"" + systemStr + "\",\"temperature\":0.0, \"max_tokens\":4096}";
} else {
    // claude-3 画像あり
    string requestData_1 = "{\"model\":\"" + model + "\", \"system\":\"" + systemStr + "\", \"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"" + question + "\"},";

<以下略>

公式サイト

[C++] 365 ChatAIアプリの製作 その46 画像エンコードデータ送信 claude-3 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]
対応LLM:gpt-4, gpt-4-vision, claude-3

claude-3でも画像のエンコードデータを送れるようにしました。JSONの形式がgpt-4と異なるため、かなり手を入れました。

画像のあるなしに関係なくclaude-3の方がレスポンスに時間が掛かります。

if (model.find("gpt") != string::npos){
    // gpt-4
    requestData = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"system\",\"content\":\"" + systemStr + "\"},{\"role\":\"user\",\"content\":\"" + question + "\"}], \"temperature\":0.0}";
} else {
    if (urls == ""){
        // claude-3 画像なし
        requestData = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"user\",\"content\":\"" + systemStr + question + "\"}], \"temperature\":0.0, \"max_tokens\":4096}";
    } else {
        // claude-3 画像あり
        string requestData_1 = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"" + systemStr + question + "\"},";
        vector<string> urlList = splitString(urls, '\n');
        cout << "urlListの要素数: " << to_string(urlList.size()) << endl;

        std::stringstream imageStream;
        for (size_t i = 0; i < urlList.size(); ++i) {
            // ローカル画像はBase64形式にエンコードする
            if (urlList[i].find("https:") != string::npos){
                cout << "claude3への画像URL送信には未対応" << endl;
                return "";
            } else {
                string base64_image = file_to_base64(urlList[i]);
                string file_extension = get_file_extension(urlList[i]);
                string url = "\"media_type\":\"image/" + file_extension + "\",\"type\":\"base64\", \"data\": \"" + base64_image + "\"";
                imageStream << "{\"type\": \"image\", \"source\": {" << url << "}}";
            }
            
            if (i < urlList.size() - 1) {
                imageStream << ",";
            }
        }

        std::string requestData_2 = imageStream.str() + "]}], \"max_tokens\": 4096,\"temperature\": 0.0}";
        cout << "requestData_2: \n" << requestData_2.c_str() << endl;

        requestData = requestData_1 + requestData_2;
    }
}

※ file_to_base64関数他については前回の記事参照

[C++] 364 ChatAIアプリの製作 その45 画像URL or エンコードデータ送信 gpt-4-vision 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]
対応LLM:gpt-4, gpt-4-vision, claude-3

これまでは自分のブログの非公開記事に画像を貼り付けて、そのURLを送信していましたが、ローカル画像をBase64形式にエンコードして送れるようにもしました。

string requestData_1 = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"system\",\"content\":\"" + systemStr + "\"},{\"role\":\"user\",\"content\":[{\"type\": \"text\", \"text\":\"" + question + "\"},";

cout << "requestData_1: \n" << requestData_1.c_str() << endl;

vector<string> urlList = splitString(urls, '\n');
cout << "urlListの要素数: " << to_string(urlList.size()) << endl;

std::stringstream imageStream;
for (size_t i = 0; i < urlList.size(); ++i) {
    // URLの場合はそのまま使用し、ローカルの場合はBase64形式にエンコードする
    if (urlList[i].find("https:") != string::npos){
        imageStream << "{\"type\": \"image_url\", \"image_url\": {\"url\":\"" << urlList[i] << "\"}}";
    } else {
        string base64_image = file_to_base64(urlList[i]);
        string file_extension = get_file_extension(urlList[i]);
        string url = "data:image/" + file_extension + ";base64," + base64_image;
        imageStream << "{\"type\": \"image_url\", \"image_url\": {\"url\":\"" << url << "\"}}";
    }
    
    if (i < urlList.size() - 1) {
        imageStream << ",";
    }
}

std::string requestData_2 = imageStream.str() + "]}], \"max_tokens\": 4096,\"temperature\": 0.0}";
cout << "requestData_2: \n" << requestData_2.c_str() << endl;

requestData = requestData_1 + requestData_2;
#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <boost/archive/iterators/ostream_iterator.hpp>
#include <fstream>
#include <sstream>
#include <string>

std::string file_to_base64(const std::string& file_path) {
    using namespace boost::archive::iterators;
    using Base64EncodeIterator = base64_from_binary<transform_width<std::string::const_iterator, 6, 8>>;
    
    std::ifstream file(file_path, std::ios::binary);
    std::string file_contents((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    
    std::stringstream os;
    std::copy(Base64EncodeIterator(file_contents.begin()), Base64EncodeIterator(file_contents.end()), ostream_iterator<char>(os));
    size_t num = (3 - file_contents.length() % 3) % 3;
    for (size_t i = 0; i < num; i++) {
        os.put('=');
    }
    return os.str();
}

std::string get_file_extension(const std::string& file_path) {
    size_t dot_pos = file_path.rfind('.');
    if (dot_pos == std::string::npos) return "";
    return file_path.substr(dot_pos + 1);
}