見出し画像

【Google Cloud】東北ずん子に質問!

東北ずん子に質問!というWebアプリを開発しましたので、その紹介をします。


1.基本動作

・非同期処理の構築による迅速なレスポンスにより、音声によるAIとのリアルな質問対応が実現しました。
・キャラクターの動作に対応した滑らかなアニメーションにより、キャラクターとの会話を体感できます。
・なお、本アプリケーションの動作は、ネットワークの通信環境等に大きく依存するため、なるべく良好な通信環境のみで利用してください。

2.仕組み

・システムアーキテクチャ、プロジェクトの機能と特徴、操作方法、sample_a_code.gs、sample_b_code.gs、sample_cloudrun_index.jsは以下のとおりです。

システムアーキテクチャ
プロジェクトの機能と特徴
操作方法

<sample_a_code.gs>

function transcribeAudioFile(fileId) {
  try {
    // Google DriveからMP3ファイルを取得
    var file = DriveApp.getFileById(fileId);
    var blob = file.getBlob();
    var base64Audio = Utilities.base64Encode(blob.getBytes());
    
    // Google Cloud Speech-to-Text APIにリクエストを送信
    var apiKey = 'Google Cloud APIキー'; // ここにGoogle Cloud APIキーを入力
    var url = 'https://speech.googleapis.com/v1/speech:recognize?key=' + apiKey;
    
    var payload = {
      "config": {
        "encoding": "MP3",
        "sampleRateHertz": 16000,  // 録音時のサンプルレートに合わせて設定
        "languageCode": "ja-JP",   // 日本語の言語コード
        "enableAutomaticPunctuation": true,  // 自動句読点を有効にする
        "speechContexts": [
          {
            "phrases": ["特定の単語", "フレーズ"]
          }
        ]  // 必要に応じて強調したい単語やフレーズを追加
      },
      "audio": {
        "content": base64Audio
      }
    };
    
    var options = {
      "method" : "post",
      "contentType": "application/json",
      "payload" : JSON.stringify(payload)
    };
    
    var response = UrlFetchApp.fetch(url, options);
    var responseText = response.getContentText();
    Logger.log("API Response: " + responseText);  // レスポンスをログに出力して確認
    var json = JSON.parse(responseText);
    
    // 取得したテキストを返す
    if (json.results && json.results.length > 0) {
      return json.results[0].alternatives[0].transcript;
    } else {
      Logger.log("No speech detected in API response.");
      return "No speech detected.";
    }
  } catch (error) {
    Logger.log("Error in transcribeAudioFile: " + error.toString());  // エラーをログに出力
    return "Error: " + error.toString();
  }
}

function doPost(e) {
  try {
    Logger.log("Request parameters: " + JSON.stringify(e.parameters)); // 送信されたパラメータをログに記録

    if (e.parameters.text && e.parameters.text.length > 0) {
      // テキスト入力を処理
      var transcript = e.parameters.text[0]; // テキストを取得
      if (!transcript || transcript.trim() === "") {
        transcript = "犬と猫の違いを教えてください。";
      } else {
        transcript = transcript.replace(/[\r\n]+/g, ' ').trim();
      }

      var geminiResponse = sendToGemini(transcript);

      return ContentService.createTextOutput(JSON.stringify({
        'result': 'success',
        'transcript': transcript,
        'geminiResponse': geminiResponse
      })).setMimeType(ContentService.MimeType.JSON);

    } else if (e.parameters.file && e.parameters.file.length > 0) {
      // 音声ファイルを処理
      var decodedFile = Utilities.base64Decode(e.parameters.file);
      var timestamp = new Date().getTime();
      var filename = 'recording_' + timestamp + '.mp3';

      var blob = Utilities.newBlob(decodedFile, 'audio/mp3', filename);
      
      // 音声ファイルをGoogle Driveに保存
      var folderId = '保存先フォルダID'; // 保存先フォルダID
      var folder = DriveApp.getFolderById(folderId);
      var file = folder.createFile(blob);
      Logger.log("File saved: " + file.getName() + " (ID: " + file.getId() + ")");

      // 音声ファイルをテキストに変換
      var transcript = transcribeAudioFile(file.getId());
      Logger.log("Transcript: " + transcript);

      if (!transcript || transcript.trim() === "") {
        transcript = "ラーメンの種類を教えてください。";
      }

      // テキストをGemini APIに送信
      var geminiResponse = sendToGemini(transcript);

      return ContentService.createTextOutput(JSON.stringify({
        'result': 'success',
        'transcript': transcript,
        'geminiResponse': geminiResponse
      })).setMimeType(ContentService.MimeType.JSON);

    } else {
      return ContentService.createTextOutput(JSON.stringify({
        'result': 'error',
        'error': 'No file or text provided.'
      })).setMimeType(ContentService.MimeType.JSON);
    }
  } catch (f) {
    Logger.log("Error in doPost: " + f.toString());
    return ContentService.createTextOutput(JSON.stringify({
      'result': 'error',
      'error': f.toString()
    })).setMimeType(ContentService.MimeType.JSON);
  }
}

function sendToGemini(transcript) {
  var url = 'Cloud FunctionsのURL';  // Cloud FunctionsのURL

  var payload = {
    "transcript": transcript
  };

  var options = {
    "method": "post",
    "contentType": "application/json",
    "payload": JSON.stringify(payload),
    "muteHttpExceptions": true
  };

  try {
    var response = UrlFetchApp.fetch(url, options);
    var responseText = response.getContentText();
    Logger.log("Gemini API Response: " + responseText);

    var json = JSON.parse(responseText);

    if (json && json.result === 'success') {
      return json.geminiResponse;
    } else {
      Logger.log("Error in Gemini API response: " + responseText);
      return "Error: " + json.message || "Unknown error occurred.";
    }
  } catch (error) {
    Logger.log("Error fetching from Gemini API: " + error.toString());
    return "Error: " + error.toString();
  }
}

<sample_b_code.gs>

function doGet(e) {
  // TTS APIキーを設定
  const ttsApiKey = 'TTS APIキー'; // ここにTTS APIキーを入力

  // レスポンスとしてAPIキーを返す
  return ContentService.createTextOutput(JSON.stringify({ key: ttsApiKey }))
    .setMimeType(ContentService.MimeType.JSON);
}

<sample_cloudrun_index.js>

const fetch = require('node-fetch');

exports.sendToGemini = async (req, res) => {
  try {
    const transcript = req.body.transcript;
    const apiKey = 'Gemini APIのキー';  // ここにGemini APIのキーを設定
    const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${apiKey}`;

    const prompt = `${transcript} あなたは東北ずん子という17歳の女性です。礼儀正しい話し方です。100字程度で簡潔に回答してください。`;

    const payload = {
      contents: [{ parts: [{ text: prompt }] }]
    };

    const options = {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    };

    const response = await fetch(url, options);
    const json = await response.json();

    if (json && json.candidates && json.candidates.length > 0) {
      const responseText = json.candidates[0].content.parts[0].text || "No response text available.";
      res.json({ result: 'success', geminiResponse: truncateText(responseText, 120) });
    } else {
      res.json({ result: 'error', message: 'No response from Gemini API or unexpected response format.' });
    }
  } catch (error) {
    res.status(500).json({ result: 'error', message: error.toString() });
  }
};

function truncateText(text, maxLength) {
  if (text.length <= maxLength) return text;
  let truncated = text.slice(0, maxLength);
  const lastPunctuation = Math.max(truncated.lastIndexOf('。'), truncated.lastIndexOf('、'), truncated.lastIndexOf(' '));
  if (lastPunctuation > 0) truncated = truncated.slice(0, lastPunctuation + 1);
  return truncated + '...';
}

3.その他

・このWebアプリは、AI Hackathon with Google Cloudに参加しています。

いいなと思ったら応援しよう!