本投稿は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:TEditとButton4:TButtonコンポーネントを配置します。フォームのサイズが変更してもよいようにAnchorsを設定してください。
画像ファイルを指定するために,OpenPictureDialog1:TOpenPictureDialogコンポーネントを配置します。
利用できる画像フォーマットを制限するため,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を編集します。
Button4にOnClickイベントを追加して以下のようにコードを書きます。
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実行ツール上で動いているモデルからの回答が表示されます。
改良のヒント
- チャット入力後にチャット内容や添付する画像のフルパスを消すと,より使いやすいと思います。
- 画像ファイルのサイズを事前に調べて,大きすぎるようなら警告または拒否する機能もあったほうが良いと思います。
- 出力結果がMarkDownになる可能性が高いので,その1で触れたようにMarkDownをHTMLで記述する処理を追加すると読みやすくなると思います。
謝辞
この記事やコードを書くにあたり,以下の記事やサイトを参考にしました。改めてこの場を借りて感謝します。
- 【Delphi】REST クライアントライブラリを使う @ht_deko(Hideaki Tominaga)
https://qiita.com/ht_deko/items/eb3f987c50012109e781 - [Delphi] JSON の処理は Serializer を使うのがナウい @pik(Jun HOSOKAWA)
https://qiita.com/pik/items/4e601c5db404d5a88dec

