4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DelphiでローカルLLMを利用したアプリを開発 その5 画像添付機能の追加

Last updated at Posted at 2025-12-11

本投稿はDelphi AdventCalender 2025 #12の記事です。

この記事は,Delphi13でコードを書き,Windows11で動作を確認しています。

はじめに

Delphi13を使ってLM StudioとOllamaの切り替えができるのLLMとチャットするアプリに画像(PNG・JPEG)を送信する機能を追加します。
この記事は,

の続きの記事で,これらで作成したUnitを使います。
これらの記事で使ったコードが前提となりますので,この記事を読む前にぜひこちらをご覧ください。

画像送信のしくみ

既にその2のKP.AICompThreadsとその4のKP.AICompUtilsに実装しているのですが,利用していませんでした。LM StudioとOllamaを統合してから使い方を説明したほうがわかりやすいと考えたからです。
チャットでテキストを送信する際にJPEGまたはPNG画像を1つ添付して送信できます。
画像処理に対応したモデル(例えばgemma3:12B)を使うと,送信した画像を踏まえた会話が可能です。
特別なデリミタ”:attach:”を使い,その後ろに画像のフルパスを記述すれば,BASE64でエンコードしてリクエストJSONに埋め込んで送信します。
ローカル処理なので処理できる画像サイズはPCが使えるVRAMサイズに依存し,あまり大きな画像は処理できません。150KBくらいの小さい画像だとうまくいくことは確認できています。コードでファイルサイズの制限はかけていません。
デモでは,その4で作ったチャットアプリの一番下に画像を添付できるしくみを追加します。

画像添付機能付きのLLMチャットツールのデモ

その4で作成したLLMチャットツールに画像添付機能を追加します。
以下のようにForm1の下部にスペースを作って,Edit2:TEditButton4:TButtonコンポーネントを配置します。フォームのサイズが変更してもよいようにAnchorsを設定してください。
画像ファイルを指定するために,OpenPictureDialog1:TOpenPictureDialogコンポーネントを配置します。

2025-12-12-01.png

利用できる画像フォーマットを制限するため,OpenPictureDialog1のFilterプロパティを以下のように編集します。

すべて (*.jpg;*.jpeg;*.png)|*.jpg;*.jpeg|JPEG イメージファイル (*.jpg)|*.jpg|JPEG イメージファイル (*.jpeg)|*.jpeg|Portable Network Graphics (*.png)|*.png

配置されたコンポーネントはそれぞれ以下の機能を持っています。
Button1: チャットの送信
Edit1: LM Studioのホスト名(プロトコルとポート番号は自動で追加されます)
Button2: ホストにダウンロードされているモデル一覧を取得する
Combobox1: モデル一覧
Button3: チャットの開始と終了
Combobox2: LLM実行ツールの一覧
Edit2: 添付される画像ファイルのフルパス
Button4: 画像ファイルを選択し,Edit2.Textに入れる

Unit1を編集します。
Button4OnClickイベントを追加して以下のようにコードを書きます。

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  System.Types, // TStringDynArray
- Vcl.Forms, Vcl.Graphics, Vcl.StdCtrls, Vcl.Controls,
+ Vcl.Forms, Vcl.Graphics, Vcl.StdCtrls, Vcl.Controls, Vcl.Dialogs, Vcl.ExtDlgs,
  Winapi.WebView2, Winapi.ActiveX, Vcl.Edge,
  KP.ListString, KP.AICompThreads, KP.AICompUtils;

type
  TForm1 = class(TForm)
    EdgeBrowser1: TEdgeBrowser;
    Edit1: TEdit;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Memo2: TMemo;
    ComboBox1: TComboBox;
    ComboBox2: TComboBox;
+   Edit2: TEdit;
+   Button4: TButton;
+   OpenPictureDialog1: TOpenPictureDialog;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
+   procedure Button4Click(Sender: TObject);
    procedure EdgeBrowser1NavigationCompleted(Sender: TCustomEdgeBrowser;
        IsSuccess: Boolean; WebErrorStatus: COREWEBVIEW2_WEB_ERROR_STATUS);
  private
    { Private 宣言 }
    AICompChat: TAICompChat;

    procedure ThreadError(AText:string);
    procedure ThreadModelListOut(AAICompModelList: TStringDynArray);

    procedure ThreadChatIn;
    procedure ThreadChatOut(AAICompParamOut:TAICompParamOut);
    procedure ThreadChatFinish;

    procedure ListStringToEdgeBrowser1;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

-uses
-  Vcl.Dialogs; // MessageDlg Interfaceに移動
  
procedure TForm1.ThreadError(AText: string);
begin
  if AText<>'' then begin
    MessageDlg(AText, TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0);
  end;
end;

procedure TForm1.ThreadModelListOut(AAICompModelList: TStringDynArray);
var
  LModel, LItem:string;
  i:integer;
begin
  LItem:=ComboBox1.Items[ComboBox1.ItemIndex];
  ComboBox1.Items.Clear;
  for LModel in AAICompModelList do begin
    ComboBox1.Items.Add(LModel);
  end;
  i := ComboBox1.Items.IndexOf(LItem);
  if i=-1 then i := 0;
  ComboBox1.ItemIndex := i;
  Button3.Enabled     := Combobox1.Items.Count>0
end;

procedure TForm1.ThreadChatIn;
begin
  Button1.Enabled   := False;
end;

procedure TForm1.ThreadChatOut(AAICompParamOut: TAICompParamOut);
begin
//  StatusDisp(AAICompParamOut); // StatusBarにTokenの情報を出力
  ListStringToEdgeBrowser1;
  Button1.Enabled := True;
end;

procedure TForm1.ThreadChatFinish;
begin
  // Terminateをした後の処理
  Button1.Enabled   := False;
  Button2.Enabled   := True;
  Button3.Enabled   := True;
  Button3.Caption   := 'Start';
  Edit1.Enabled     := True;
  ComboBox1.Enabled := True;
  ComboBox2.Enabled := True;
end;

procedure TForm1.ListStringToEdgeBrowser1;
var
  s:string;
begin
  // https://mam-mam.net/delphi/tedgebrowser.html
  s:=ChatQAToHTML(AICompChat.StringListQ, AICompChat.StringListA);
  if not EdgeBrowser1.WebViewCreated then
  begin
    EdgeBrowser1.CreateWebView;
    while not EdgeBrowser1.WebViewCreated do begin
      Application.ProcessMessages;
      Sleep(100);
    end;
  end;
  EdgeBrowser1.NavigateToString(s);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo2.Lines.TrailingLineBreak := False;

  Edit1.Text       := 'localhost';
  Button1.Enabled  := False;
  Button2.Enabled  := True;
  Button3.Enabled  := False;
  Button3.Caption  := 'Start';

  AICompChat := TAICompChat.Create;
  Combobox2.Items.Text := AICompTypeText; // ローカルLLMフレームワークのタイプ
  Combobox2.ItemIndex  := 0;
+
+  Edit2.Text           := ''; // 空だと画像を添付する処理は実行されない
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  AICompChat.Free;
end;

procedure TForm1.EdgeBrowser1NavigationCompleted(Sender: TCustomEdgeBrowser;
    IsSuccess: Boolean; WebErrorStatus: COREWEBVIEW2_WEB_ERROR_STATUS);
begin
  // 一番下までスクロールする
  EdgeBrowser1.ExecuteScript('window.scroll(0, Number.MAX_SAFE_INTEGER);');
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  if Memo2.Lines.Text='' then exit;
  if not AICompChat.isThreadStart then begin
    Button3Click(Self);
  end;
- AICompChat.ChatSend(Memo2.Lines.Text);
+ AICompChat.ChatSend(Memo2.Lines.Text, Edit2.Text); // 画像を添付する機能を追加

//  Memo2.Lines.Text := '';
//  edtAttach1.Text:='';
  ListStringToEdgeBrowser1;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  LHost:string;
begin
  LHost:= PrivateHostNameMod(Edit1.Text, IndexToAICompType(ComboBox2.ItemIndex));
  if LHost='' then exit;

  TAICompModelList.Create(IndexToAICompType(ComboBox2.ItemIndex), LHost, ThreadModelListOut, ThreadError);
end;

procedure TForm1.Button3Click(Sender: TObject);
var
  LHost:string;
  LAICompParamIn:TAICompParamIn;
begin
  LHost:= PrivateHostNameMod(Edit1.Text, IndexToAICompType(ComboBox2.ItemIndex));
  if LHost='' then exit;
  if Combobox1.Text='' then exit;
  if AICompChat.isThreadStart then begin
    AICompChat.ChatClose; // ThreadChatFinishイベントでTerminate後の処理を書く
  end else begin
    LAICompParamIn.Init;
    with LAICompParamIn do begin
      AICompType   := IndexToAICompType(ComboBox2.ItemIndex);
      Host         := LHost;
      Model        := ComboBox1.Text;
      SystemPrompt := '';
      Prompt       := '';
      Timeout      := 30*1000; // タイムアウトは30秒
      num_ctx      := 4098;    // コンテキスト長 LM_Studioでは使わない
    end;

    AICompChat.Open(LAICompParamIn,
      ThreadChatIn, ThreadChatOut, ThreadChatFinish, ThreadError);

    Button1.Enabled   := True;
    Button2.Enabled   := False;
    Button3.Caption   := 'Stop';
    Edit1.Enabled     := False;
    ComboBox1.Enabled := False;
    ComboBox2.Enabled := False;
  end;
end;

+procedure TForm1.Button4Click(Sender: TObject);
+begin
+  if OpenPictureDialog1.Execute then begin
+    Edit2.Text := OpenPictureDialog1.FileName;
+  end;
+end;

end.

Combobo2でLLM実行ツールを選択し,Edit1にLocalhostや192.168.1.20などのLLM実行ツールを動かしているPCのIPアドレスを入力し,Button2をクリックするとComboBox1にモデルの一覧が表示されます。
画像処理が利用できる利用したいモデル(例えばgemma3:12B)を選んで,Button3をクリックして,Memo2に質問を書き,Button4をクリックして処理が可能そうな小さな画像を選択して,Button1をクリックすると,入力した質問がEdgeBrowser1に表示され,LLM実行ツール上で動いているモデルからの回答が表示されます。

2025-12-12-02.png

改良のヒント

  • チャット入力後にチャット内容や添付する画像のフルパスを消すと,より使いやすいと思います。
  • 画像ファイルのサイズを事前に調べて,大きすぎるようなら警告または拒否する機能もあったほうが良いと思います。
  • 出力結果がMarkDownになる可能性が高いので,その1で触れたようにMarkDownをHTMLで記述する処理を追加すると読みやすくなると思います。

謝辞

この記事やコードを書くにあたり,以下の記事やサイトを参考にしました。改めてこの場を借りて感謝します。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?