【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に参加しています。