画像をPOSTして、内容を解析して貰う
Claude3のAPIは画像のPOSTに対応しています。画像をPOSTするにはどうしたらよいのでしょうか?APIリファレンスを見るとBase64でエンコードして、JSONのcontent属性の中にdata属性として埋め込むようです。
【JSONフォーマット】
{
model : "claude-3-opus-20240229",
max_tokens : 1024,
messages: [
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "/9j/4AAQSkZJRg...",
}
},
{
"type": "text",
"text": "What is in this image?"
}
]
}
]
}
Notes上に実装するには画像を選択する必要がありますので、Notesフォームを拡張し、画像のファイルパスが入るフィールドと、画像を選択するためのボタンを作ります。
【Notesフォーム】
Base64にエンコードする関数を作る
LotusScriptにはBase64に変換してくれる関数がないようなので自作します。またファイル拡張子からMediaTypeを判定する関数も作ります。
【LotusScriptソース-Base64エンコード関数】
Function Base64Encode(vImageData As Variant) As String
'---------- ---------- ---------- ---------- ----------
' 画像のBinaryをBase64エンコードした文字列に変換する
' 引数 :vImageData - 画像Binary
' 戻り値 :Base64エンコード済み画像
'---------- ---------- ---------- ---------- ----------
Dim objXml As Variant
Dim objElem As Variant
Set objXml = CreateObject("MSXML2.DOMDocument")
' Base64エンコードを行う要素を作成
Set objElem = objXml.createElement("b64")
objElem.DataType = "bin.base64"
objElem.nodeTypedValue = vImageData
' Base64エンコードされた文字列を返す
Base64Encode = Replace(objElem.Text, Chr(10), "")
Set objElem = Nothing
Set objXml = Nothing
End Function
【LotusScriptソース-MediaType判定関数】
Function GetMediaType(sFilePath As String) As String
'---------- ---------- ---------- ---------- ----------
' ファイルの拡張子からMediaTypeを決定する
' 引数 :sFilePath - ファイルパス
' 戻り値 :MediaType文字列(image/jpeg、image/png、image/gif、image/webp
'---------- ---------- ---------- ---------- ----------
Dim sExt As String '拡張子
Dim sMediaType As String 'メディアタイプ
sExt = Lcase(Strrightback(sFilePath , "."))
Select Case sExt
Case "jpg","jpeg":
sMediaType = "image/jpeg"
Case "png":
sMediaType = "image/png"
Case "gif":
sMediaType = "image/gif"
Case "webp":
sMediaType = "image/webp"
Case Else
sMediaType = "Unknown"
End Select
GetMediaType = sMediaType
End Function
【LotusScriptソース-送信ボタン】
Sub Click(Source As Button)
'---------- ---------- ---------- ---------- ----------
'Claude3 APIから取得したレスポンスを書込み-画像
'
'---------- ---------- ---------- ---------- ----------
Const APIURL = "https://api.anthropic.com/v1/messages"
Const APIKEY = "(取得したAPIKEY)"
Const MODEL = "claude-3-opus-20240229"
Const MAXTOKENS = 1024
'クラス・変数宣言
Dim ws As New NotesUIWorkspace
Dim uidoc As NotesUIDocument
Dim doc As NotesDocument
Dim session As New NotesSession
Dim jsonNav As NotesJSONNavigator
Dim vXml As Variant 'XMLオブジェクト
Dim sRequest As String '入力した質問
Dim sBodyJson As String '送信するJSON文字列
Dim sContent As String '回答本文
Dim sImagePath As String '画像ファイルパス
Dim sBase64Str As String 'Base64変換後ファイル
Dim vImageData As Variant '画像ファイルBinary
Dim sMediaType As String 'メディアタイプ(POSTで使用)
Dim vFileStream As Variant 'ADODB.Stream
'クラス・変数セット
Set uidoc = ws.CurrentDocument
Set doc = uidoc.Document
'入力チェック
sRequest = doc.Request(0)
sImagePath = doc.ImagePath(0)
If sRequest = "" Or sImagePath = "" Then
Msgbox "質問を入力して下さい。"
Exit Sub
End If
'ファイルの読み込み1-ADODB.Stream
Set vFileStream = CreateObject("ADODB.Stream")
vFileStream.Type = 1 'バイナリ形式
vFileStream.Open
vFileStream.LoadFromFile sImagePath
' バイナリデータを変数に読み込む
vImageData = vFileStream.Read
Set vFileStream = Nothing
'Base64エンコード
sBase64Str = Base64Encode(vImageData)
'画像ファイルのMediaType決定
sMediaType = GetMediaType(sImagePath)
'Getリクエストを実行し、結果を取得
Set vXml = CreateObject("MSXML2.XMLHTTP")
vXml.Open "POST", APIURL, False
vXml.setRequestHeader "Content-Type", "application/json"
vXml.setRequestHeader "x-api-key", APIKEY
vXml.setRequestHeader "anthropic-version", "2023-06-01"
sBodyJson = |{"model":"| & MODEL _
& |","max_tokens": | & MAXTOKENS _
& |, "messages":[ |_
& |{"role":"user", "content":[| _
& | {"type":"image","source": { "type":"base64","media_type":"| & sMediaType & |","data":"| & sBase64Str & |"} },| _
& | {"type":"text", "text": "| & sRequest & |"} | _
& |] } | _
& |] | _
& |}|
vXml.send sBodyJson
'エラーレスポンスチェック
If vXml.Status <> 200 Then
Msgbox vXml.responseText , 16 , "Error : " & Cstr(vXml.Status)
Exit Sub
End If
'レスポンスから回答を抽出
Set jsonNav = session.CreateJSONNavigator(vXml.responseText)
sContent = jsonNav.GetElementByPointer("/content/0/text").Value
'回答をフォームに書込み
doc.HttpStatus = vXml.Status
doc.Response = sContent
End Sub
【元画像】
【実行結果】
大きい画像を読み込むとVariantがエラーになる
画像をPOSTし、分析する機能は実装できたのですが、大きな画像をPOSTするとNotesクライアントがエラーを出力するようです。デバッグモードで見るとファイルを読み込んでVariant型変数に代入するところでエラーになっています。Byte型の配列に書き換えてみましたが「64k制限:Click:(行数):I037 配列またはリストのリファレンスが不正です。(変数名)」エラーが出て、ソースの保存をさせて貰えません。下記エラーの「最大領域」というのは「64k制限」のことを言っているんでしょうね。
VBAで実装したものを移植していますが、VBAでは正常に動きます。なぜかというとVBA6.0までは64k制限がありましたが、VBA7.0(Office2010)で2GBまで緩和され、大容量の変数を持つことができるようになったためです。
【Notesクライアントエラーメッセージ】
【デバックモードによるエラー箇所】
64k制限の回避とNotesの裏コマンド
上記の制限についてNotesConsortium大阪研究会の浜さんに相談したところ、LotusScriptのNotesStreamクラスにはBase64にエンコードするための「裏コマンド、裏メソッド」があると言うことを教えて貰いました。このメソッドはDesignerHelpには掲載されていません。
こういう裏コマンド、メソッドがあるなら、サポート対象内にして貰って、DesignerHelpも整備して欲しいですね。
【LotusScriptソース-NotesStreamクラス裏メソッド利用部分】
Function StreamToBase64(streamIn As NotesStream) As String
On Error Goto theOldWay
' ReadEncoded function is not documented. In case it doesn't work have a backup.
StreamToBase64 = Replace(streamIn.ReadEncoded(ENC_BASE64, 76), Chr$(13), "") '←【裏メソッド】
Exit Function
theOldWay:
Dim session As New NotesSession
Dim db As NotesDatabase
Dim doc As NotesDocument
Dim mime As NotesMIMEEntity
Set db = session.CurrentDatabase
Set doc = db.CreateDocument
Set mime = doc.CreateMIMEEntity("Body")
streamIn.Position = 0
Call mime.SetContentFromBytes(streamIn, "image/gif", ENC_NONE)
mime.EncodeContent(ENC_BASE64)
StreamToBase64 = Replace(mime.ContentAsText, Chr$(13), "")
End Function
でも、動かない!なぜだ!?
サクッと実装できるかと思いましたが、動きません。こういう場合は、動くものまで立ち返る必要があります。どうやらBase64でエンコードした後の文字列がおかしい感じがするので、エンコード後の文字列をPythonとLotusScriptでテキストファイルに出力して比較してみます。
【Notesクライアントエラーメッセージ】
【Python-エンコード後文字列】
【LotusScript-エンコード後文字列】
どうやら改行コードが入っているようです。改行コードをReplace関数でNullに変換してPOSTすると正常に動作しました。NotesStreamクラスでBase64変換ができるのであれば、わざわざ「MSXML2.DOMDocument」を使ってBase64変換する必要はなかったですね。
【LotusScriptコード-送信ボタン】
Sub Click(Source As Button)
'---------- ---------- ---------- ---------- ----------
'Claude3 APIから取得したレスポンスを書込み-画像
'
'---------- ---------- ---------- ---------- ----------
Const APIURL = "https://api.anthropic.com/v1/messages"
Const APIKEY = "(取得したAPIKEY)"
Const MODEL = "claude-3-opus-20240229"
Const MAXTOKENS = 1024
'クラス・変数宣言
Dim ws As New NotesUIWorkspace
Dim uidoc As NotesUIDocument
Dim doc As NotesDocument
Dim session As New NotesSession
Dim jsonNav As NotesJSONNavigator 'JSON解析
Dim stream As NotesStream '画像ファイル読み込み'
Dim vXml As Variant 'XMLオブジェクト
Dim sRequest As String '入力した質問
Dim sBodyJson As String '送信するJSON文字列
Dim sContent As String '回答本文
Dim sImagePath As String '画像ファイルパス
Dim sBase64Str As String 'Base64変換後ファイル
Dim sMediaType As String 'メディアタイプ(POSTで使用)
'クラス・変数セット
Set uidoc = ws.CurrentDocument
Set doc = uidoc.Document
'入力チェック
sRequest = doc.Request(0)
sImagePath = doc.ImagePath(0)
If sRequest = "" Or sImagePath = "" Then
Msgbox "質問を入力して下さい。"
Exit Sub
End If
'画像ファイルの読み込みとBase64エンコード
Set stream = session.CreateStream()
Call stream.Open(sImagePath, "binary")
sBase64Str = Replace(stream.ReadEncoded(ENC_BASE64, 76), Chr(13) & Chr(10) , "") '裏コード
'画像ファイルのMediaType決定
sMediaType = GetMediaType(sImagePath)
'Getリクエストを実行し、結果を取得
Set vXml = CreateObject("MSXML2.XMLHTTP")
vXml.Open "POST", APIURL, False
vXml.setRequestHeader "Content-Type", "application/json"
vXml.setRequestHeader "x-api-key", APIKEY
vXml.setRequestHeader "anthropic-version", "2023-06-01"
sBodyJson = |{"model":"| & MODEL _
& |","max_tokens": | & MAXTOKENS _
& |, "messages":[ |_
& |{"role":"user", "content":[| _
& | {"type":"image","source": { "type":"base64","media_type":"| & sMediaType & |","data":"| & sBase64Str & |"} },| _
& | {"type":"text", "text": "| & sRequest & |"} | _
& |] } | _
& |] | _
& |}|
vXml.send sBodyJson
'エラーレスポンスチェック
If vXml.Status <> 200 Then
Msgbox vXml.responseText , 16 , "Error : " & Cstr(vXml.Status)
Exit Sub
End If
'レスポンスから回答を抽出
Set jsonNav = session.CreateJSONNavigator(vXml.responseText)
sContent = jsonNav.GetElementByPointer("/content/0/text").Value
'回答をフォームに書込み
doc.HttpStatus = vXml.Status
doc.Response = sContent
End Sub
【実行結果】
最後に
生成AIは今、一番ホットな話題でたくさん情報が出ています。最終的には複数社に収束していくのだと思いますが、情報がたくさんあり、みんなが試行錯誤している時に学んで実験しておくのが、一番楽しく、乗り遅れない方法だと思います。皆さんも生成AIがどのように活用できるか?実験してみて下さい。