1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google Forms(GAS)で大容量ファイルをGCSにアップロード

Last updated at Posted at 2024-08-17

はじめに

Google Cloudの生成AI「Gemini」を活用すれば、動画や音声ファイルを入力して、議事録を作成するアプリを作ることができます。
そのためには、まず、Google Cloud Storage(以降GCS)にファイルをアップロードしてから、Vertex AIのGeminiで処理する必要があります。
この記事では、Google Formsを使って、大容量の動画ファイルをGCSにアップロードする方法をご紹介します。

ファイルサイズ制限を回避するために

Google Apps Script(以降GAS)を使ってGCSにファイルをアップロードしようとしたのですが、GASにはファイルサイズ50MBまでしか送れないという制限があります。
1時間のオンライン会議動画のファイルサイズは、数百MB程度あるので、この制限にひっかかってしまいます。
そこで、Google Formsの「送信」をトリガーにして、ファイルIDとファイル名の情報のみをGoogle Cloud Functonsのエンドポイントに送って、Cloud Functionsがファイルのアップロード処理を実行する方法を採用しました。

Google Formsの準備

まず、Google Formsのコンポーネントとして「ファイルのアップロード」を選択します。

スクリーンショット 2024-08-16 21.54.49.png

このフォームでは、アップロードしたファイルは、フォーム作成者の Google ドライブに保存されます。
許可するファイル形式、最大のファイルサイズ(10GBまで)、フォームの合計ファイルサイズ(1TBまで)を指定することが可能です。

スクリーンショット 2024-08-16 22.04.41.png

このフォームからファイルをアップロードする場合は、PC端末またはGoogleドライブのファイルを選択できます。

スクリーンショット 2024-08-16 22.10.21.png

アップロードしたファイルの保存先のURLがスプレッドシートに登録されます。
続いて、このスプレッドシートの拡張機能から Apps Scriptを作成していきます。

スクリーンショット 2024-08-16 22.42.20.png

Google Apps Script(GAS)の作成

ファイルIDとファイル名をGoogle Cloud Functionsに渡すためのGASを作成します。
「Googleドライブ」にあるGoogle Meetの動画ファイルなどは、拡張子がついていないため、ファイル名を渡す際に要注意です。
今回は、mp4形式の動画ファイルに限定してアップロードしたかったため、拡張子がない場合は、mp4をつける処理を記載しています。

send-file-data.gs
function sendRequestToEndpoint() {
  Logger.log("処理を開始します...");

  // スプレッドシートから必要な情報を取得
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRow = sheet.getLastRow();
  Logger.log(`スプレッドシートから最終行を取得: ${lastRow}`);
  
  // 最終行のデータを取得
  const fileUrl = sheet.getRange(lastRow, 2).getValue(); // ファイルURL
  Logger.log(`取得したファイルURL: ${fileUrl}`);
    
  // ファイルIDをGoogleドライブのURLから抽出
  const fileId = fileUrl.match(/[-\w]{25,}/)[0]; // ファイルIDを抽出
  Logger.log(`抽出したファイルID: ${fileId}`);
  
  // ファイルIDからファイル名を取得
  let fileName = getFileNameFromFileId(fileId);
  
  if (!fileName) {
    Logger.log("ファイル名の取得に失敗しました。");
    return;
  }
  Logger.log(`取得したファイル名: ${fileName}`);

  // ファイル名に拡張子がない場合は追加
  if (!fileName.toLowerCase().endsWith('.mp4')) {
    fileName += '.mp4';
    Logger.log(`拡張子がなかったため、ファイル名を修正しました: ${fileName}`);
  }

  // Cloud Functionを呼び出してGoogle DriveからGCSへのファイルアップロードを依頼
  Logger.log("Cloud Functionにファイルアップロードを依頼中...");
  const uploadResult = triggerFileUploadToCloudFunction(fileId, fileName);

  if (!uploadResult.success) {
    Logger.log("ファイルアップロードに失敗しました: " + uploadResult.message);
    return;
  }

  Logger.log("ファイルのアップロードが成功しました。");
}

function getFileNameFromFileId(fileId) {
  try {
    // Google Driveのファイルを取得
    const file = DriveApp.getFileById(fileId);
    
    // ファイル名を取得
    let fileName = file.getName();
    
    // ファイル名に.mp4拡張子がない場合は追加
    if (!fileName.toLowerCase().endsWith('.mp4')) {
      fileName += '.mp4';
    }
    
    Logger.log(`ファイルID ${fileId} に対応するファイル名(.mp4確認後): ${fileName}`);
    return fileName;
  } catch (e) {
    // エラー処理
    Logger.log("ファイル名の取得中にエラーが発生しました: " + e.toString());
    return null;
  }
}

function triggerFileUploadToCloudFunction(fileId, fileName) {
  try {
    const cloudFunctionUrl = "<Cloud FunctionのエンドポイントのURL>"
    const scriptProperties = PropertiesService.getScriptProperties();
    const apiKey = scriptProperties.getProperty('API_KEY');
    
    // ファイル名に.mp4拡張子がない場合は追加(再確認)
    if (!fileName.toLowerCase().endsWith('.mp4')) {
      fileName += '.mp4';
    }

    const payload = {
      fileId: fileId,
      fileName: fileName
    };

    const options = {
      method: 'post',
      contentType: 'application/json',
      headers: {
        'x-api-key': apiKey // スクリプトプロパティから取得したAPIキーを使用
      },
      payload: JSON.stringify(payload)
    };

    const response = UrlFetchApp.fetch(cloudFunctionUrl, options);
    
    if (response.getResponseCode() === 200) {
      return { success: true };
    } else {
      return { success: false, message: response.getContentText() };
    }
  } catch (e) {
    return { success: false, message: e.toString() };
  }
}

Cloud FunctionsのエンドポイントのURLの入力
このあとの作業でCloud Functionsのデプロイをしますので、発行されるエンドポイントのURLを入力してください。

APIキーの作成
推測されにくいランダムな文字列をAPI_KEYとして、スクリプトプロパティに保管します。
スクリプトプロパティは、サイドメニューの「プロジェクトの設定」を開いて、一番下にあります。

スクリーンショット 2024-08-16 23.24.26.png

トリガーの設定
サイドメニューの「トリガー」を開いて、新しいトリガーを作成します。
今回は、フォームの送信時にGASを起動させたいので、「フォーム送信時」を選択します。

スクリーンショット 2024-08-16 23.29.53.png

右下の保存ボタンを押すと、認証を求める画面に遷移するので、画面の指示に従って、認証して下さい。
以下のようなトリガーが作成されます。

スクリーンショット 2024-08-17 6.55.12.png

Cloud Functionsのスクリプト作成

続いて、今回はPythonで、Cloud Functionsのスクリプトを作成します。GASからファイルIDとファイル名が渡されたら、実際にGoogleドライブのファイルをダウンロードして、GCSへのアップロードを実行する処理になります。セキュリティを確保するため、APIキーを使って認証しています。

main.py
import functions_framework
from google.cloud import storage, secretmanager
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseDownload
import io
import os
import json

# シークレットマネージャーからシークレットを取得する関数
def get_secret(secret_id):
    client = secretmanager.SecretManagerServiceClient()
    name = f"projects/{os.getenv('GCP_PROJECT')}/secrets/{secret_id}/versions/latest"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("UTF-8")

# 環境変数からバケット名を取得
BUCKET_NAME = os.getenv('BUCKET_NAME')

# Cloud Functionsのエントリーポイント
@functions_framework.http
def upload_file_from_drive_to_gcs(request):
    # APIキーの検証
    api_key = request.headers.get('x-api-key')
    if api_key != get_secret('API_KEY'):
        return ("認証エラー", 401)

    # サービスアカウントの認証情報を取得
    service_account_key = get_secret("GOOGLE_APPLICATION_CREDENTIALS")
    credentials = service_account.Credentials.from_service_account_info(json.loads(service_account_key))

    # リクエストからファイルIDとファイル名を取得
    data = request.get_json()
    file_id = data.get('fileId')
    file_name = data.get('fileName')
    if not file_id or not file_name:
        return ("ファイルIDとファイル名が必要です", 400)

    # Google Drive APIクライアントの初期化
    drive_service = build('drive', 'v3', credentials=credentials)

    # Google Driveからファイルをダウンロード
    request = drive_service.files().get_media(fileId=file_id)
    fh = io.BytesIO()
    downloader = MediaIoBaseDownload(fh, request)
    done = False
    while not done:
        status, done = downloader.next_chunk()
        print(f"ダウンロード進捗: {int(status.progress() * 100)}%")

    # ダウンロードしたファイルのストリームを先頭に戻す
    fh.seek(0)

    # Google Cloud Storageクライアントの初期化
    storage_client = storage.Client(credentials=credentials)
    bucket = storage_client.bucket(BUCKET_NAME)
    blob = bucket.blob(file_name)

    # ファイルをGoogle Cloud Storageにアップロード
    try:
        blob.upload_from_file(fh, content_type='video/mp4')
        return (f"ファイル {file_name}{BUCKET_NAME} に正常にアップロードしました。", 200)
    except Exception as e:
        print(f"GCSへのファイルアップロード中にエラーが発生しました: {e}")
        return ("GCSへのファイルアップロード中にエラーが発生しました", 500)

Cloud FunctionsのHTTPリクエストの最大サイズは32MBですが、上記の方法ですと、HTTPリクエストで渡されるのは、ファイルIDとファイル名のみですので、問題ありません。この処理では、アップロードしたファイルをメモリに保持しているので、メモリの設定が重要です。Cloud Functionsのメモリは128MB〜32GBの範囲で設定できます。

続いて、requirements.txtに必要なライブラリを記載します。

requirements.txt
functions-framework==3.*
google-cloud-storage
google-cloud-secret-manager
google-auth
google-api-python-client

GCS、シークレットマネージャ、Google APIの認証に関連するライブラリを追加しています。

Cloud Functions のデプロイ

作成したスクリプトをCloud Functionsにデプロイします。
GCPのCloud functionsのコンソールから、「ファンクションを作成」を押して、以下の項目を設定します。

  • 関数名 upload_file_from_drive_to_gcs
  • リージョン asia-northeast1
  • トリガーのタイプ HTTPS
  • 割り当てられるメモリ 2GiB  ※アップロードするファイルのサイズを考慮して指定
  • ランタイムサービスアカウント
    • あらかじめ作成しておいたサービスアカウントを指定
  • ランタイム環境変数
    • BUCKET_NAME バケット名を指定
    • GCP_PROJECT プロジェクト名を指定
  • 接続 内向き(上り) すべてのトラフィックを許可する
  • ランタイム python3.12
  • エントリポイント upload_file_from_drive_to_gcs
  • ソース main.pyrequirements.txtを編集して、上記のコードを入力

サービスアカウントの権限設定

GCPのサービスアカウントを作成して、以下の権限を追加します。

  • Secret Manager のシークレット アクセサー
  • ストレージ管理者

続いて、サービスアカウントキーを発行して、サービスアカウントキーとなるjsonファイルをダウンロードします。

参考記事

シークレットの作成

シークレットマネージャから以下のシークレットをそれぞれ作成します。これらのシークレットは、上記のCloud Functionsの関数内で読み取っています。

  • GOOGLE_APPLICATION_CREDENTIALS サービスアカウントキーのJSONファイルをアップロード
  • API_KEY GASのスクリプトプロパティに保管したAPIキーを「シークレットの値」として入力

スクリーンショット 2024-08-17 10.23.29.png

Google Drive APIの設定

GCPのコンソールでGoogle Drive APIを有効化します。

スクリーンショット 2024-08-17 10.13.13.png

Google Drive側での共有

次にGoogle Driveのファイルをダンロードできる権限をサービスアカウントに付与します。

  • GCPのコンソールのサイドメニューで「IAMと管理」=>「サービスアカウント」と進むと、作成したサービスアカウントのメールアドレスを確認して、コピーします
  • 最初に作成したGoogleフォームの「ファイルをアップロード」欄の右下の「フォルダを表示」から、ファイルが保存されるフォルダを開くことができます

スクリーンショット 2024-08-17 10.50.44.png

  • フォルダを開いて、このフォルダの共有設定から、「編集者」権限をサービスアカウントのメールに付与します

スクリーンショット 2024-08-17 10.57.08.png

この画面の共有設定から、「サービスアカウント」のメールアドレスを入力して、「編集者」の権限をつけることができます。

おわりに

以上の方法でGoogleドライブやPCのローカルフォルダから、大容量ファイルをGCSにアップロードすることができるはずです。
私の場合、別途、動画ファイルから、Geminiで議事録を作成する処理をCloud Runにデプロイしています。GAS側のコードにCloud Runのエンドポイントを追加して、emailを送信する処理を追加すれば、ユーザーがGoogleフォームからファイルをアップロードした数分後に議事録のメールが届きます。
また、Googleフォームを工夫すれば、Geminiのモデル選択、パラメーター設定、プロンプト入力もできる簡易的なインターフェイスとして、活用できます。
最後までお読みいただき、ありがとうございました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?