「
Google Gemini に相談
」を編集中 (節単位)
ナビゲーションに移動
検索に移動
警告:
ログインしていません。編集を行うと、あなたの IP アドレスが公開されます。
ログイン
または
アカウントを作成
すれば、あなたの編集はその利用者名とともに表示されるほか、さまざまなメリットもあります。
スパム攻撃防止用のチェックです。 決して、ここには、値の入力は
しない
でください!
==== 上記のマクロから呼び出されるマクロ ==== ファイル名は「call_gemini_api.js」とすること。 <syntaxhighlight lang="javascript" copy> #language = "v8" #title = "Gemini に相談 (API 呼び出し部品)" BeginUndoGroup(); // 生成AIの回答の言語設定 // "Japanese" や "English" を設定できます。他の言語は未検証。 // 自動設定にする場合は空文字 "" を設定してください。 const LANGUAGE = "Japanese"; // 入力プロンプトの最大トークン数 // 超適当なトークナイザー関数により計測し、英単語は4文字1トークン、それ以外は1文字1トークンとして数えます。 const MAX_PROMPT_TOKEN = 8000; // 会話の履歴を保持する数 (質問と回答で1セット) // 数を増やすと、会話の履歴が増えますが、Meryのメモリ使用量が増えたり、 // 使用トークン数が増えて動作が重くなる可能性があるため、適宜調整してください。 // document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。 const HISTORY_SIZE = 0; // 会話履歴に保持する最大トークン数 // 会話履歴のトークン数がこの数を超える場合、古い履歴から削除されます。 const MAX_HISTORY_TOKEN = 270000; // 生成AI応答の入力毎の遅延時間 (ミリ秒) // 入力をゆっくりにしたいときは数字を大きくしてください。 const INPUT_DELAY = 25; // 利用する生成AIのモデル // const AI_MODEL = "models/gemini-2.5-flash-preview-04-17"; const AI_MODEL = "models/gemini-2.5-flash-lite-preview-06-17"; // 生成AIのリクエストパラメータ const GENERATION_CONFIG = { max_output_tokens: 6000, temperature: 0.65, thinkingConfig: { includeThoughts: false, thinkingBudget: 1000 } }; // テンプレート利用の場合のtemperature const TEMPERATURE_TEMPLATE = 0.25; // タグ名の定義 const HISTORY_TAG = "gemini-history"; const TEMPORARY_CUSTOM_INSTRUCTION_TAG = "gemini-temporary-custom-instruction"; const SELECTED_TEMPLATE_PROMPT_TAG = "gemini-selected-template-prompt"; // APIキー環境変数名 const API_KEY_ENV = "GEMINI_API_KEY" // console.log に出力する内容を Mery のアウトプットバーに表示する const console = { log: function (str) { outputBar.Writeln(str) } } const doc = document; const sel = document.selection; // 生成リクエスト中止フラグ let abortRequestFlg = false // 生成リクエスト完了フラグ; let requestCompletedFlg = false; main(); async function main() { let promptText = sel.Text.trim(); // 文字数が多すぎる場合はエラー if (roughTokenize(promptText) > MAX_PROMPT_TOKEN) { alert("プロンプトの文字数が多すぎます。文字数を少なくしてください。"); return; } // テンプレートの場合はプロンプトを変更 const isTemplate = doc.Tag.exists(SELECTED_TEMPLATE_PROMPT_TAG); if (isTemplate) { promptText = "# **指示**:\n\n" + doc.Tag[SELECTED_TEMPLATE_PROMPT_TAG] + "\n\n# **入力テキスト**:\n\n" + promptText; } try { // 処理終了を待機 shell.KeepRunning = true; // カーソル位置移動 sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical)); sel.EndOfLine(false, mePosLogical); sel.NewLine(); sel.Text = "\n---\n\n" document.KeepScrollPos = true; // APIキーの取得 const apiKey = shell.getEnv(API_KEY_ENV); if (!apiKey) { alert(`${API_KEY_ENV} が設定されていません`); return; } // Gemini API に問い合わせ await Promise.all([ callGeminiAPI(apiKey, promptText, isTemplate), abortRequest() ]); if (sel.GetActivePointX(mePosLogical) > 2) { sel.Text = "\n\n---\n\n\n" } else { sel.Text = "\n---\n\n\n" } } catch (e) { console.log("エラーが発生しました: " + e.message + "\n" + e.stack); sel.Text = "<処理中にエラー発生>" } finally { // 処理終了待機を解除 shell.KeepRunning = false; } } // Shift キーが長押しされた場合はリクエスト中止フラグを立てる async function abortRequest() { const VIRTUAL_KEY_CODE_SHIFT = 0x10; // Shift キーの仮想キーコード let pressCount = 0; while (!requestCompletedFlg) { await new Promise(resolve => setTimeout(resolve, 300)); if (shell.GetKeyState(VIRTUAL_KEY_CODE_SHIFT) < 0) { pressCount++; if (pressCount > 2) { abortRequestFlg = true; return; } } } } async function callGeminiAPI(apiKey, promptText, isTemplate) { const controller = new AbortController(); const signal = controller.signal; try { // fetch で Gemini API にリクエスト const requestBody = { contents: createGeminiRequest(promptText, isTemplate), system_instruction: buildSystemInstruction(), generationConfig: GENERATION_CONFIG, }; if (isTemplate) { // テンプレートの場合は応答を決定論的にする // テンプレート以外の場合はデフォルト値とするために指定しない requestBody.generationConfig.temperature = TEMPERATURE_TEMPLATE; } const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/${AI_MODEL}:streamGenerateContent?alt=sse&key=${apiKey}`, { signal: signal, method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); let buffer = ""; let responseAll = ""; let usageText = ""; const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { // リクエスト中止フラグが立っている場合は中断 if (abortRequestFlg) { controller.abort(); sel.Text = "<処理を中止>"; return null; } const { done, value } = await reader.read(); if (done) { // 最後に残ったバッファを処理 if (buffer.trim().length > 0) { // 最後の不完全なJSONを処理(可能であれば) try { const lastLine = buffer.trim().replace(/^data:\s*/, ''); if (lastLine) { const processedData = processLine(lastLine, responseAll); if (processedData) { sel.Text = processedData.inputText || ""; usageText = processedData.usageText || ""; responseAll = processedData.responseAll || responseAll; } } } catch (e) { console.log("不完全なJSONの処理でエラー: " + e.message); } } break; } // 受信したバイナリデータ (value) をテキストに変換する const text = decoder.decode(value); // 前回のバッファと今回のテキストを結合 const combinedText = buffer + text; // 完全な行を探す(最後の改行の位置を見つける) const lastNewlineIndex = combinedText.lastIndexOf('\n'); if (lastNewlineIndex === -1) { // 改行がない場合は全てバッファに保存 buffer = combinedText; continue; } // 完全な行を処理 const completeText = combinedText.substring(0, lastNewlineIndex); // 残りをバッファに保存 buffer = combinedText.substring(lastNewlineIndex + 1); // テキストには複数のメッセージが含まれている可能性があるため、 // 改行 (LF) で分離して、1 行ずつ処理する const lines = completeText.trim().split(/\n+/); let inputText = ""; for (const line of lines) { // HACK 空行が渡ってくることがあるので trim() を入れる const json_text = line.trim().replace(/^data:\s*/, ''); // データがない場合はスキップ if (!json_text) { continue; } const processedData = processLine(json_text, responseAll); if (processedData) { inputText += processedData.inputText || ""; usageText = processedData.usageText || ""; responseAll = processedData.responseAll || responseAll; } else if (processedData === null) { // JSONパースエラーが発生した場合は、バッファをクリアして終了 buffer = ""; inputText = "JSONパースに失敗\n" + completeText; break; } } if (inputText.length > 0) { // 上記までで数文字まとまって取得されるので、まとめて入力 sel.Text = inputText; } // 入力遅延を入れる await new Promise(resolve => setTimeout(resolve, INPUT_DELAY)); } if (usageText) { sel.Text = (responseAll.slice(-1) === "\n" ? "\n" : "\n\n") + `(${usageText})`; } // 結果を会話履歴に追加 if (HISTORY_SIZE > 0 && !isTemplate && responseAll.length > 0) { appendReturnTextToTag(promptText, responseAll); } } catch (e) { controller.abort(); throw e; } finally { requestCompletedFlg = true; } } // 一行を処理する関数を分離して再利用可能にする function processLine(json_text, responseAll) { try { const data = JSON.parse(json_text); const candidate = data.candidates[0]; let updatedResponseAll = responseAll; // まとまった文字数で入力する const response = candidate.content.parts[0].text let inputText = formatGeminiResponse(response, responseAll.slice(-5)); if (inputText) { updatedResponseAll += inputText; } // 応答が終了した場合はループを抜ける let usageText = ""; if (candidate.finishReason === "STOP") { const usage = data.usageMetadata; usageText = "利用トークン数: プロンプト=" + usage.promptTokenCount + ", 応答=" + usage.candidatesTokenCount + ", 思考=" + usage.thoughtsTokenCount + ", 合計=" + usage.totalTokenCount; } return { inputText, usageText, responseAll: updatedResponseAll }; } catch (e) { console.log("JSONパースエラー: " + e.message + ", データ: " + json_text); return null; } } // Geminiの応答には半角スペースが連続して含まれることがあるため、整形する function formatGeminiResponse(response, lastFiveChars) { let formattedResponse = response .replace(/([。!?]) +/g, "$1") .replace(/([*:.]) {2,}/g, "$1 "); const joinedLastFiveChars = lastFiveChars + formattedResponse; if (joinedLastFiveChars.match(/([。!?]) +/)) { formattedResponse = formattedResponse.replace(/^ +/, ""); } if (joinedLastFiveChars.match(/([*:.]) {2,}/)) { if (lastFiveChars.slice(-1) === " ") { formattedResponse = formattedResponse.replace(/^ +/, ""); } else { formattedResponse = formattedResponse.replace(/^ +/, " "); } } return formattedResponse } function roughTokenize(text) { // 超適当なトークナイザ。トークン数をざっくりと多めに数える // 空白文字・ハイフン・アンダースコア区切りの単語に分割して、 // 単語がアルファベットのみ、数字のみの場合には 4文字=1トークン として数える。 // 単語にアルファベット以外の文字が含まれる場合は 1文字=1トークン として数える。 return text .split(/[\s\-_]+/) .map(w => /^[a-zA-Z]+$|^[0-9]+$|/.test(w) ? Math.ceil(w.length / 4) : w.length) .reduce((a, b) => a + b, 0); } // document.Tag の内容を加味してGeminiのリクエストを作成 function createGeminiRequest(promptText, isTemplate) { // テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない if (isTemplate) { return [createRoleAndParts("user", promptText)]; } const tag = doc.Tag; // 以前の会話が存在しない場合は新規リクエスト if (!tag.exists(HISTORY_TAG)) { const roleAndParts = [createRoleAndParts("user", promptText)]; return roleAndParts } // 会話履歴が存在する場合は、履歴を含めてリクエスト const roleAndParts = JSON.parse(tag[HISTORY_TAG]); roleAndParts.push(createRoleAndParts("user", promptText)); return roleAndParts; } function createRoleAndParts(role, text) { return { "role": role, "parts": [ { "text": text } ] }; } // document.Tag にGeminiの応答を追加 function appendReturnTextToTag(userText, modelText) { const tag = doc.Tag; if (!tag.exists(HISTORY_TAG)) { tag[HISTORY_TAG] = JSON.stringify([ createRoleAndParts("user", userText), createRoleAndParts("model", modelText) ]); } else { const geminiText = JSON.parse(tag[HISTORY_TAG]); geminiText.push(createRoleAndParts("user", userText)); geminiText.push(createRoleAndParts("model", modelText)); // 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除 if (geminiText.length > HISTORY_SIZE * 2) { geminiText.splice(0, 2); } // 会話履歴全体が MAX_HISTORY_TOKEN を超えた場合、最初のセットを削除 let joinedText = geminiText.map(e => e.parts[0].text).join("\n"); while (roughTokenize(joinedText) > MAX_HISTORY_TOKEN) { geminiText.splice(0, 2); joinedText = geminiText.map(e => e.parts[0].text).join("\n"); } tag[HISTORY_TAG] = JSON.stringify(geminiText); } } function buildSystemInstruction() { let customInstruction = getTemporaryCustomInstruction(); const language = LANGUAGE.trim(); customInstruction = customInstruction.replace(/^[\s ]+|[\s ]+$/g, ""); return { parts: { text: "Do not hallucinate. If you are unsure or don't have enough information to answer with confidence, say \"I don't know\" or \"I'm not sure.\"\n" + (customInstruction ? `${customInstruction}\n` : "") + (language ? `Respond in ${language}.\n` : "") } } } function getTemporaryCustomInstruction() { const tag = doc.Tag; if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) { return tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG]; } else { return ""; } } </syntaxhighlight>
編集内容の要約:
MeryWikiへの投稿はすべて、他の投稿者によって編集、変更、除去される場合があります。 自分が書いたものが他の人に容赦なく編集されるのを望まない場合は、ここに投稿しないでください。
また、投稿するのは、自分で書いたものか、パブリック ドメインまたはそれに類するフリーな資料からの複製であることを約束してください(詳細は
MeryWiki:著作権
を参照)。
著作権保護されている作品は、許諾なしに投稿しないでください!
このページを編集するには、下記の数式を計算してその答えを欄に入力してください (
ヘルプ
):
いちたすには =
キャンセル
編集ヘルプ
(新しいウィンドウで開きます)
スポンサーリンク
ナビゲーション メニュー
個人用ツール
ログインしていません
トーク
投稿記録
アカウント作成
ログイン
名前空間
ページ
議論
日本語
表示
閲覧
編集
履歴表示
その他
検索
スポンサーリンク
スポンサーリンク
案内
メインページ
ヘルプ
よくある質問
マクロリファレンス
マクロライブラリ
プラグインライブラリ
構文ファイル
テーマ
寄付・開発支援
練習用ページ
開発室
開発者のブログ
ツール
スポンサーリンク