🔍

Google Apps Script を用いた Gemini によるフレキシブルテンプレート

2024/02/27に公開2

Abstract

Gemini API の新しい「セマンティック検索」機能は、コーパスから必要な情報を取得するために使用できます。Google Apps Script でこれらの機能を使用するのは少し複雑でしたが、ライブラリを作成することでスクリプトの簡素化が可能になります。 このレポートでは、このライブラリを生成コンテンツとともに使用して、Google ドキュメント, Google スライド のテンプレート処理へ応用させることで、より柔軟なテンプレート処理の可能性を示します。

Introduction

セマンティック検索は、期待値を見つけるための新たな手法の一つです。 最近、コーパスを管理するための API が Gemini API に追加されました。Ref Gemini API のコーパスを使用すると、効率的にセマンティック検索を実現できます。Ref ただし、コーパスを Google Apps Script で利用しようとすると、スクリプトが少し複雑になってしまいます。これに対処するために、Google Apps Script を使用してコーパスを管理するためのライブラリを作成しました。Ref このライブラリを使用すると、シンプルなスクリプトでコーパスの管理が可能になります。

この投稿では、Gemini API のコーパスと生成コンテンツによるセマンティッ検索を、Google ドキュメント, Google スライド を用いたテンプレート処理に適用しています。 これにより、より柔軟なテンプレート処理実現の可能性を示唆します。

フレキシブルテンプレート処理のワークフロー

セマンティック検索を利用したフレキシブルテンプレート処理を実現するために次のようなワークフローを使用します。 このレポートでは、テンプレートを使用して画像を配置することを目的とします。

  1. 画像を準備します。 画像を Google ドライブ上のフォルダに入れます。
  2. 生成コンテンツを使用して画像の説明を作成します。
  3. コーパスを作成します。
  4. コーパス内にドキュメントを作成します。
  5. 作成した説明をファイルのメタデータを含めてドキュメントに保存します。
  6. データを含むドキュメントを使用してセマンティック検索を使用してテンプレート処理を実行します。

2から6の全てに対して Gemini API が使われます。サンプル画像は以下のようにGeminiで生成しました。

使用法

ここで紹介するスクリプトをテストするには、以下の流れを行ってください。

1. Google Apps Script プロジェクトを作成

スタンドアロンの Google Apps Script プロジェクトを作成してください。 このスクリプトはコンテナーバインドされたスクリプトでも使用できます。そして、Google Apps Script プロジェクトのスクリプトエディタを開いてください。

2. Google Cloud Platform プロジェクトを Google Apps Script プロジェクトにリンク

この場合、これを行う方法は私のリポジトリで確認できます。

また、API コンソールで Generative Language API を有効にしてください。

3. Google Apps Script ライブラリ (CorporaApp) をインストール

このライブラリのインストール方法は私のリポジトリで確認できます。

4. データをコーパスに保存する

テンプレート処理の前に、データをコーパスに保存する必要があります。

このサンプルではテンプレート処理として画像を使用します。テンプレートのプレースホルダーを、用意した画像データと置換させます。このため、初めに Google ドライブ内のフォルダーに画像ファイルを準備してください。

次のスクリプトをコピーしてスクリプトエディタに貼り付け、関数 main の folderId へそのフォルダ ID をセットしてください。

const createCorpus_ = (_) =>
 CorporaApp.createCorpus({
   name: "corpora/sample-corpus",
   displayName: "sample corpus",
 });

const createDocument_ = (_) =>
 CorporaApp.createDocument("corpora/sample-corpus", {
   name: "corpora/sample-corpus/documents/sample-document",
   displayName: "sample document",
 });

/**
* ### Description
* Generate text from text and image.
* ref: https://medium.com/google-cloud/automatically-creating-descriptions-of-files-on-google-drive-using-gemini-pro-api-with-google-apps-7ef597a5b9fb
*
* @param {Object} object Object including API key, text, mimeType, and image data.
* @return {String} Generated text.
*/
function getResFromImage_(object) {
 const { token, text, mime_type, data } = object;
 const url = `https://generativelanguage.googleapis.com/v1/models/gemini-pro-vision:generateContent`;
 const payload = {
   contents: [{ parts: [{ text }, { inline_data: { mime_type, data } }] }],
 };
 const options = {
   payload: JSON.stringify(payload),
   contentType: "application/json",
   headers: { authorization: "Bearer " + token },
 };
 const res = UrlFetchApp.fetch(url, options);
 const obj = JSON.parse(res.getContentText());
 if (obj.candidates.length > 0 && obj.candidates[0].content.parts.length > 0) {
   return obj.candidates[0].content.parts[0].text;
 }
 return "No response.";
}

// Please run this function.
function main() {
 const folderId = "###"; // Please set the folder ID of the folder including images.

 const documentResourceName =
   "corpora/sample-corpus/documents/sample-document";
 createCorpus_(); // Create corpus as "corpora/sample-corpus".
 createDocument_(); // Create document into the corpus as "corpora/sample-corpus/documents/sample-document".

 // 1. Retrieve description of the images using Gemini API.
 const requests = [];
 const files = DriveApp.getFolderById(folderId).searchFiles(
   "trashed=false and mimeType contains 'image/'"
 );
 const token = ScriptApp.getOAuthToken();
 while (files.hasNext()) {
   const file = files.next();
   const fileId = file.getId();
   const url = `https://drive.google.com/thumbnail?sz=w1000&id=${fileId}`;
   const bytes = UrlFetchApp.fetch(url, {
     headers: { authorization: "Bearer " + token },
   }).getContent();
   const base64 = Utilities.base64Encode(bytes);
   const description = getResFromImage_({
     token,
     text: "What is this image? Explain within 50 words.",
     mime_type: "image/png",
     data: base64,
   });
   console.log(description);
   if (description == "No response.") continue;
   requests.push({
     parent: documentResourceName,
     chunk: {
       data: { stringValue: description.trim() },
       customMetadata: [
         { key: "fileId", stringValue: fileId },
         { key: "url", stringValue: file.getUrl() },
       ],
     },
   });
 }
 if (requests.length == 0) return;

 // 2. Put descriptions to document as chunks.
 const res = CorporaApp.setChunks(documentResourceName, { requests });
 console.log(JSON.stringify(res.map((r) => JSON.parse(r.getContentText()))));
}

このスクリプトを実行すると、コーパスとドキュメントが作成され、フォルダー内の画像の説明も作成されます。 そして、その説明は、コーパス内に作成したドキュメントへ保存されます。このドキュメントは次のスクリプトで使用されます。

5. Google ドキュメント のテンプレート処理

このスクリプトを使用する前に、以下のテンプレートを含む Google ドキュメント を用意してください。

サンプルスクリプトは次の通りです。Google ドキュメント の Google ドキュメント ID を documentId にセットしてください。上記のスクリプトを実行すると、documentResourceNamecorpora/sample-corpus/documents/sample-document になります。

function semanticSearch_withDocuments() {
 const documentId = "###"; // Please set your Google Document ID.
 const documentResourceName =
   "corpora/sample-corpus/documents/sample-document";

 const body = DocumentApp.openById(documentId).getBody();
 const table = body.getTables()[0];
 for (let r = 1; r < table.getNumRows(); r++) {
   const a = table.getCell(r, 0);
   const searchText = a.getText().trim();
   console.log(searchText);
   const res = CorporaApp.searchQueryFromDocument(documentResourceName, {
     query: searchText,
     resultsCount: 1,
   });
   const { relevantChunks } = JSON.parse(res.getContentText());
   if (!relevantChunks || relevantChunks.length == 0) return;
   const { data, customMetadata } = relevantChunks[0].chunk;
   console.log(data.stringValue);
   const fileId = customMetadata.find(({ key }) => key == "fileId");
   const blob = DriveApp.getFileById(fileId.stringValue).getBlob();
   a.appendImage(blob)
     .setWidth(100)
     .setHeight(100)
     .getParent()
     .asParagraph()
     .setAlignment(DocumentApp.HorizontalAlignment.CENTER);
   a.getChild(0).removeFromParent();
   table.getCell(r, 1).setText(data.stringValue);
 }
}

このスクリプトを上記のテンプレートドキュメント に対して実行すると次のような結果が得られます。この場合、{{###}}のようなプレースホルダーのテキストをそのまま検索テキストとして使用できるようです。

6. Google スライド のテンプレート処理

このスクリプトを使用する前に、以下のテンプレートを含む Google スライド を準備してください。

サンプルスクリプトは以下の通りです。Google スライド の Google スライド ID を presentationId に設定してください。

function semanticSearch_withSlides() {
 const presentationId = "###"; // Please set your Google Slide ID.
 const documentResourceName =
   "corpora/sample-corpus/documents/sample-document";

 const s = SlidesApp.openById(presentationId);
 const slide = s.getSlides()[0];
 slide.getShapes().forEach((s, i) => {
   console.log(`--- Shape ${i + 1}`);
   const searchText = s.getText().asString().trim();
   console.log(searchText);
   const res = CorporaApp.searchQueryFromDocument(documentResourceName, {
     query: searchText,
     resultsCount: 1,
   });
   const { relevantChunks } = JSON.parse(res.getContentText());
   if (!relevantChunks || relevantChunks.length == 0) return;
   const { data, customMetadata } = relevantChunks[0].chunk;
   console.log(data.stringValue);
   const fileId = customMetadata.find(({ key }) => key == "fileId");
   const blob = DriveApp.getFileById(fileId.stringValue).getBlob();
   s.replaceWithImage(blob);
 });
}

このスクリプトを上記のテンプレートスライド に対して実行すると、次のような結果が得られます。

Note

  • すでに必要な画像素材を持っている場合には、説明文からのエンベディングがコーパス内にすでに作成されているため、処理コストが低く、この方法が有効であると思われます。
  • 近いうちに Gemini API でもイメージ作成が可能になり、処理コストもより削減されると思われます。 その際には、画像ストックを使わずに上記の方法が直接実現できるのではないかと思われます。
Google Cloud Japan

Discussion

hankei6kmhankei6km

セマンティック検索の記事とライブラリありがとうございます。GAS で簡単に試すことができて助かっています。

本筋ではないことで恐縮ですが、記事内のスクリプトは DriveApp(Drive Service)を利用されているため、リンクした Google Cloud プロジェクトの方で Google Drive API を有効化する必要がありました。

エラー	Exception: We're sorry, a server error occurred. Please wait a bit and try again.
main	@ コード.gs:51

https://developers.google.com/apps-script/reference/drive?hl=ja

記事内に Google Drive API についての記述があると、これからテストする人もエラーを回避しやすいかもしれません。

TanaikeTanaike

コメントありがとうございます。新たなプロジェクトを作成して動作確認を行いましたが、残念ながらコメントの状況は再現できませんでした。

下記につきまして、

記事内のスクリプトは DriveApp(Drive Service)を利用されているため、リンクした Google Cloud プロジェクトの方で Google Drive API を有効化する必要がありました。

今の場合、Google Apps Script projectを作成し、スクリプトエディタへサンプルスクリプトをコピーし、下記のステップで実行します。

  1. 新たなGoogle Apps Script projectを作成し、スクリプトエディタ上でGCPとリンクする。

  2. ライブラリのCorporaAppをインストールする。この時点で、下記スコープが自動で追加されます。

    https://www.googleapis.com/auth/generative-language.retriever
    https://www.googleapis.com/auth/script.external_request
    
  3. サンプルスクリプトをスクリプトエディタへコピーペーストし、保存する。この時点で、下記スコープが自動で追加されます。

    https://www.googleapis.com/auth/drive.readonly
    https://www.googleapis.com/auth/documents
    https://www.googleapis.com/auth/presentations
    
  4. 関数mainを実行する。

  5. Template document, slideを用意し、関数semanticSearch_withDocuments, semanticSearch_withSlidesを実行する。

この流れで、エラーは発生せず、問題なく動作していることを再度確認しました。このサンプルスクリプトではDrive service (DriveApp)は使用しているものの、Drive APIは使用しておりません。ただし、Drive APIのスコープであるhttps://www.googleapis.com/auth/drive.readonlyは使用しています。このため、特にDrive APIを有効にせずとも動作します。ただ、もしもGoogle workspace下のアカウントで動作テストを行われている場合、サーバ側の設定によっては何らかのアクションが必要になる可能性も否定できません。(これが、Drive APIの有効化に関係しているかどうかは不明です。申し訳ありません。)

また、エラーメッセージについて、残念ながらコード.gs:51については分かりませんが、Exception: We're sorry, a server error occurred. Please wait a bit and try again.のエラーメッセージからメッセージ通り、再度実行することで解決する可能性はないでしょうか。Gemini APIの場合、時々このようなエラーが発生するようです。私の方で何度かスクリプトを実行してみた際はエラーは発生せず、表示されたエラーは再現できませんでした。重ねて申し訳ありません。

引き続き、下記を調べてみたいと思います。再現できましたら、追記させていただきます。

  • エラーメッセージ Exception: We're sorry, a server error occurred. Please wait a bit and try again.がコンスタントに再現可能かどうか。
  • エラーメッセージ Exception: We're sorry, a server error occurred. Please wait a bit and try again.がDrive APIの有効無効に関係しているかどうか。
  • Drive APIを有効にする必要があるかどうか。

貴重なコメントありがとうございました。