ken_nogi/pleasanter/develop/留守電変換/sample.js
Kenichiro NOGI 88a402ce0f up
2026-07-10 18:13:30 +09:00

221 lines
7.7 KiB
JavaScript

/*
@title :GoogleAI 音声解析スクリプト
@author :Refactored
*/
// ==========================================
// 設定定義 (メンテナンスしやすくするために定数化)
// ==========================================
const CONFIG = {
// ⚠️ APIキーは必ず再生成し、ここには直接書かず、可能であればパラメータ機能等を利用してください
KEYS: {
GEMINI: "AIzaSyD7M3XgGaTXn1mYg3CktBhF7kTRnxTgKAQ",
PLEASANTER: "6504c8a807677a3a576e10327f3c19876c55736ee45d4a845796b9e7f5e087bfd4bd0d8184863da8cf1733ca635111432ab334aea59102b06a96ee6d2c05190d"
},
URLS: {
GEMINI: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
// 自身のサイトのドメインに合わせて調整してください
PLEASANTER_BIN: "https://nextoffice.next-hd.co.jp/pleasanter/api/binaries/"
},
// 結果を格納するフィールドのマッピング
FIELDS: {
ATTACHMENT: "AttachmentsA",
TITLE: "ClassC",
SUMMARY: "DescriptionA",
HARASSMENT_PCT: "NumA",
MEASURE: "DescriptionB",
TODO: "DescriptionC"
},
// Geminiへの指示プロンプト
PROMPT: `
この音声データは電話の通話録音です。
この2名の会話を次の分析項目の通り分析して回答してください。
【分析項目】
・本会話のタイトル(50文字以内)
・会話の要約(200文字以内)
・カスハラ該当度パーセンテージ(数値のみ)
・本カスハラに対する対応策(200文字以内)
・話者2名に求められる今後必要なアクション(200文字以内)
【制約事項】
回答は必ず以下のJSON形式のみを出力してください。Markdown記法(code block)は含めないでください。
{"Title": "...", "Summary": "...", "CustomerHarassmentPercentage": 0, "CustomerMeasure": "...", "Todo": "..."}
無言電話で2名の会話が成立していない場合は、Summaryを「無言電話」とし、CustomerHarassmentPercentageを0、CustomerMeasureを「カスハラ該当なし」としてください。
`
};
// ==========================================
// メイン処理 (エントリポイント)
// ==========================================
try {
// ログ出力(デバッグ用)
context.Log(`Start Script: ControlId=${context.ControlId}, Depth=${context.ServerScript.ScriptDepth}`);
// 無限ループ防止チェック (ScriptDepth === 0 の時のみ実行)
if (context.ServerScript.ScriptDepth === 0) {
// 対象のボタン/イベントの場合のみ実行
const validTriggers = ["CreateBtn", "UpdateBtn", "Process_1"];
if (validTriggers.includes(context.ControlId)) {
executeGoogleAiAnalysis();
}
}
} catch (e) {
context.Log("Error in Main Scope: " + e.message);
}
// ==========================================
// ロジック関数群
// ==========================================
/**
* AI解析のメインフロー
*/
function executeGoogleAiAnalysis() {
try {
// 1. 添付ファイルの確認
const attachments = JSON.parse(model[CONFIG.FIELDS.ATTACHMENT] || "[]");
if (attachments.length === 0) {
context.Log("No attachments found.");
// 必要であればメッセージを表示
// context.AddMessage("音声ファイルが添付されていません。");
return;
}
const targetFile = attachments[0];
context.Log(`Processing File: ${targetFile.Guid}`);
// 2. 音声ファイルのBase64取得
const base64Data = fetchFileBase64(targetFile.Guid);
if (!base64Data) {
throw new Error("Failed to retrieve file content.");
}
// 3. Gemini APIへリクエスト
const aiResponse = callGeminiApi(base64Data);
if (!aiResponse) {
throw new Error("Failed to get response from AI.");
}
// 4. 結果をプリザンターの項目へセット
applyResultToModel(aiResponse);
} catch (e) {
context.Log(`Error in executeGoogleAiAnalysis: ${e.message}`);
context.AddMessage(`AI解析エラー: ${e.message}`);
}
}
/**
* プリザンター内部APIを叩いて添付ファイルのBase64を取得
*/
function fetchFileBase64(guid) {
const url = `${CONFIG.URLS.PLEASANTER_BIN}${guid}/get`;
httpClient.RequestHeaders.Clear();
httpClient.RequestUri = url;
httpClient.Content = JSON.stringify({
"ApiVersion": 1.1,
"ApiKey": CONFIG.KEYS.PLEASANTER
});
try {
const response = httpClient.Post();
const json = JSON.parse(response);
if (json.StatusCode && json.StatusCode !== 200) {
context.Log(`API Error: ${json.Message}`);
return null;
}
return json.Response.Base64;
} catch (e) {
context.Log(`Fetch File Error: ${e.message}`);
return null;
}
}
/**
* Gemini APIへの問い合わせ
*/
function callGeminiApi(base64Data) {
const url = `${CONFIG.URLS.GEMINI}?key=${CONFIG.KEYS.GEMINI}`;
const requestBody = {
"contents": [{
"parts": [
{ "text": CONFIG.PROMPT },
{
"inline_data": {
"mime_type": "audio/x-wav", // 必要に応じて wav/mp3 等の判別ロジックを入れてください
"data": base64Data
}
}
]
}],
"generationConfig": {
"responseMimeType": "application/json"
}
};
httpClient.RequestHeaders.Clear();
httpClient.RequestUri = url;
httpClient.Content = JSON.stringify(requestBody);
try {
const responseStr = httpClient.Post();
const responseJson = JSON.parse(responseStr);
// エラーレスポンスのチェック
if (responseJson.error) {
throw new Error(`Gemini API Error: ${responseJson.error.message}`);
}
// テキスト抽出
let rawText = responseJson.candidates[0].content.parts[0].text;
// 配列で返ってくる場合やMarkdownコードブロックが含まれる場合のクリーニング
// (responseMimeType: json を指定しているため、通常はそのままパース可能だが念のため)
return parseGeminiJson(rawText);
} catch (e) {
context.Log(`Gemini Call Error: ${e.message}`);
throw e;
}
}
/**
* GeminiからのJSONテキストを安全にパースする
*/
function parseGeminiJson(text) {
try {
// 万が一Markdown記法 (```json ... ```) が残っていた場合除去する
const cleanText = text.replace(/```json/g, "").replace(/```/g, "").trim();
const parsed = JSON.parse(cleanText);
// 配列で返ってきた場合は最初の要素を使う
if (Array.isArray(parsed)) {
return parsed[0];
}
return parsed;
} catch (e) {
context.Log("JSON Parse Error. Raw Text: " + text);
throw new Error("AIの回答をJSONとして解析できませんでした。");
}
}
/**
* 解析結果をモデルに適用して更新フラグを立てる
*/
function applyResultToModel(data) {
model[CONFIG.FIELDS.TITLE] = data.Title;
model[CONFIG.FIELDS.SUMMARY] = data.Summary;
model[CONFIG.FIELDS.HARASSMENT_PCT] = data.CustomerHarassmentPercentage;
model[CONFIG.FIELDS.MEASURE] = data.CustomerMeasure;
model[CONFIG.FIELDS.TODO] = data.Todo;
// 更新して終了
model.UpdateOnExit = true;
context.Log("Model updated successfully.");
}