Open5
GASからGemini File APIにファイルをアップロードする
Geminiに画像や動画をアップロードすることでマルチモーダルにできる
しかし、公式ドキュメントにはGo/Nodeのパッケージ経由のExampleしかなく、どのようにRESTAPIを叩けばよいのかわからなかった
結果的には以下のExampleを見つけた
これをGAS版に修正し、動くようにしたものがこちら
const BASE_URL = 'https://generativelanguage.googleapis.com';
const CHUNK_SIZE = 8 * 1024 * 1024; // 8 MiB
/**
* メイン関数:ファイルの検索とアップロードを実行
* @return {Object|null} アップロード結果のJSONオブジェクト、またはnull(失敗時)
*/
function uploadFileToGenAI() {
const apiKey = getApiKey();
if (!apiKey) return null;
const file = findFileToUpload();
if (!file) return null;
const uploadUrl = startResumableUpload(apiKey, file);
if (!uploadUrl) return null;
const uploadResult = uploadFileInChunks(file, uploadUrl);
if (uploadResult) {
Logger.log('アップロード結果:');
Logger.log(JSON.stringify(uploadResult, null, 2));
}
return uploadResult;
}
// getApiKey() と findFileToUpload() 関数は変更なし
/**
* レジューマブルアップロードを開始
*/
function startResumableUpload(apiKey, file) {
const url = `${BASE_URL}/upload/v1beta/files?key=${apiKey}`;
const options = {
method: 'post',
headers: {
'X-Goog-Upload-Protocol': 'resumable',
'X-Goog-Upload-Command': 'start',
'X-Goog-Upload-Header-Content-Length': file.getSize().toString(),
'X-Goog-Upload-Header-Content-Type': file.getMimeType(),
'Content-Type': 'application/json'
},
payload: JSON.stringify({ file: { display_name: file.getName() } }),
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
if (response.getResponseCode() === 200) {
const uploadUrl = response.getHeaders()['x-goog-upload-url'];
Logger.log(`レジューマブルアップロードURL: ${uploadUrl}`);
return uploadUrl;
} else {
Logger.log(`エラー: レジューマブルアップロードの開始に失敗しました。ステータスコード: ${response.getResponseCode()}`);
Logger.log(`レスポンス: ${response.getContentText()}`);
}
} catch (e) {
Logger.log(`例外が発生しました: ${e.toString()}`);
}
return null;
}
/**
* ファイルをチャンクに分けてアップロード
* @return {Object|null} アップロード結果のJSONオブジェクト、またはnull(失敗時)
*/
function uploadFileInChunks(file, uploadUrl) {
const fileSize = file.getSize();
const numChunks = Math.ceil(fileSize / CHUNK_SIZE);
const fileBlob = file.getBlob();
for (let i = 0; i < numChunks; i++) {
const offset = i * CHUNK_SIZE;
const chunkSize = Math.min(CHUNK_SIZE, fileSize - offset);
const chunkBlob = fileBlob.getBytes().slice(offset, offset + chunkSize);
const isLastChunk = (i === numChunks - 1);
Logger.log(`チャンク ${i + 1}/${numChunks} をアップロード中 (${offset} - ${offset + chunkSize} / ${fileSize})...`);
const chunkResult = uploadChunk(uploadUrl, chunkBlob, offset, chunkSize, fileSize, isLastChunk);
if (!chunkResult) {
Logger.log(`エラー: チャンク ${i + 1}/${numChunks} のアップロードに失敗しました。`);
return null;
}
if (isLastChunk) {
return chunkResult; // 最後のチャンクの結果を返す
}
}
Logger.log('アップロードが完了しましたが、最終レスポンスを取得できませんでした。');
return null;
}
/**
* 単一のチャンクをアップロード
* @return {Object|true|false} 最後のチャンクの場合はJSONオブジェクト、それ以外はboolean
*/
function uploadChunk(uploadUrl, chunkData, offset, chunkSize, totalSize, isLastChunk) {
const chunkBlob = Utilities.newBlob(chunkData, 'application/octet-stream');
const options = {
method: 'put',
headers: {
'X-Goog-Upload-Offset': offset.toString(),
'X-Goog-Upload-Command': isLastChunk ? 'upload, finalize' : 'upload',
'Content-Range': `bytes ${offset}-${offset + chunkSize - 1}/${totalSize}`
},
payload: chunkBlob,
contentLength: chunkSize,
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(uploadUrl, options);
const responseCode = response.getResponseCode();
if (isLastChunk && responseCode === 200) {
Logger.log('最終チャンクのアップロードに成功しました。レスポンスを解析します。');
try {
const jsonResponse = JSON.parse(response.getContentText());
return jsonResponse;
} catch (e) {
Logger.log(`JSONの解析に失敗しました: ${e.toString()}`);
return null;
}
} else if (!isLastChunk && responseCode === 308) {
Logger.log(`チャンクのアップロードに成功しました。レスポンスコード: ${responseCode}`);
return true;
} else {
Logger.log(`エラー: チャンクのアップロードに失敗しました。ステータスコード: ${responseCode}`);
Logger.log(`エラーレスポンス: ${response.getContentText()}`);
return false;
}
} catch (e) {
Logger.log(`チャンクのアップロード中に例外が発生しました: ${e.toString()}`);
return false;
}
}
/**
* API キーを取得
*/
function getApiKey() {
const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
if (!apiKey) {
Logger.log('エラー: GEMINI_API_KEY がスクリプトプロパティに設定されていません。');
return null;
}
return apiKey;
}
/**
* アップロードするファイルを検索
*/
function findFileToUpload() {
const files = DriveApp.searchFiles("title contains 'iom-ropo-sqm' and (mimeType contains 'audio/' or mimeType contains 'video/')");
if (!files.hasNext()) {
Logger.log('エラー: 条件に一致するファイルが見つかりません。');
return null;
}
const file = files.next();
Logger.log(`ファイルが見つかりました: ${file.getName()} (${file.getMimeType()}, ${file.getSize()} バイト)`);
return file;
}
/**
* ファイルをアップロードし、アップロードされたファイルのURIを返す
* @return {string|null} アップロードされたファイルのURI、またはnull(失敗時)
*/
function uploadFileAndGetUri() {
const uploadResult = uploadFileToGenAI();
if (!uploadResult) {
Logger.log('ファイルのアップロードに失敗しました。');
return null;
}
try {
const fileInfo = uploadResult.file;
if (fileInfo && fileInfo.uri) {
Logger.log(`アップロードされたファイルのURI: ${fileInfo.uri}`);
return fileInfo.uri;
} else {
Logger.log('アップロード結果にURIが含まれていません。');
Logger.log('完全なアップロード結果:', JSON.stringify(uploadResult, null, 2));
return null;
}
} catch (e) {
Logger.log(`URIの抽出中にエラーが発生しました: ${e.toString()}`);
Logger.log('完全なアップロード結果:', JSON.stringify(uploadResult, null, 2));
return null;
}
}
最終的には以下のようなJSONがAPIから帰ってくるはず。
{
"file": {
"name": "files/xxxxxxxxxx",
"displayName": "log",
"mimeType": "video/mp4",
"sizeBytes": "2129068",
"createTime": "2024-07-15T12:25:44.888574Z",
"updateTime": "2024-07-15T12:25:44.888574Z",
"expirationTime": "2024-07-17T12:25:44.822960401Z",
"sha256Hash": "xxxxxxxxxxxx==",
"uri": "https://generativelanguage.googleapis.com/v1beta/files/xxxxx",
"state": "PROCESSING"
}
}
このuriをGeminiのリクエストに付与してあげることで、マルチモーダルを実現できる。