Open5

GASからGemini File APIにファイルをアップロードする

ようかんようかん

これを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のリクエストに付与してあげることで、マルチモーダルを実現できる。