AI Studio to VOICEVOX

通称:スタ自ボ(Google AI スタジオ自動ボイスボックス読み上げ)

目的:AI Studioで自動音声読み上げ(VOICEVOX)が欲しい人向け
機能:読み上げ話者変更・回答後に自動で音声を読み上げるON/OFF・音量&速度調整

使用手順:①Tampermonkeyのプラグインを インストール
②「スタ自ボインストール」を押して、スクリプトインストールを行う(またはTampermonkeyで、以下のコードを新規保存)
③VOICEVOXを起動する→https://voicevox.hiroshiba.jp/
Google Studio AIを起動して読み上げできるのかプレイしてみてね

うまくいくと以下の様に自動で読み上げが始まります
C

Tampermonkeyが良く分からない人コチラの下部の解説を参考に

詳しくは下部

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
// ==UserScript==
// @name         AI Studio to VOICEVOX (v5.3.1 安定版)
// @namespace    https://greasyfork.org/scripts/555209/
// @version      5.3.1
// @description  AI Studioの回答をVOICEVOXで自動読み上げ (画像URLの読み上げバグを完全修正)
// @author       FoeverPWA
// @match        https://aistudio.google.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      localhost
// @license      MIT
// @downloadURL https://update.greasyfork.org/scripts/555209/AI%20Studio%20to%20VOICEVOX.user.js
// @updateURL https://update.greasyfork.org/scripts/555209/AI%20Studio%20to%20VOICEVOX.meta.js
// ==/UserScript==

(function() {
    'use strict';

    // ===== 設定 =====
    const CONFIG_KEY = 'aistudio_voicevox_config';
    const defaultConfig = {
        apiUrl: 'http://localhost:50021',
        speakerId: 20,
        autoPlay: true,
        speedScale: 1.0,
        volumeScale: 1.0
    };

    let savedConfig = GM_getValue(CONFIG_KEY, {});
    let config = Object.assign({}, defaultConfig, savedConfig);

    // ===== 状態管理 =====
    let state = {
        isPlaying: false,
        isSynthesizing: false,
        currentAudio: null,
        audioQueue: [],
        textQueue: [],
        lastProcessedText: '',
        lastProcessedCleanText: '',
        textBuffer: '', // 【重要】読み上げ前のテキストバッファ
        wasGenerating: false, // 前回の生成状態を保持
        playButton: null,
        shouldStop: false
    };

    // ===== ユーティリティ関数 =====
    function getModelResponseText() {
        const turns = document.querySelectorAll('ms-chat-turn');
        const lastTurn = turns[turns.length - 1];
        if (!lastTurn) return '';
        const container = lastTurn.querySelector('[data-turn-role="Model"]');
        if (!container) return '';
        const textChunks = Array.from(container.querySelectorAll('ms-text-chunk'));
        let text = '';
        textChunks.forEach(chunk => {
            if (chunk.closest('ms-code-chunk, ms-thought-chunk')) return;
            text += chunk.textContent;
        });
        return text;
    }

    function isGenerating() {
        const turns = document.querySelectorAll('ms-chat-turn');
        const lastTurn = turns[turns.length - 1];
        if (!lastTurn) return false;
        return !lastTurn.querySelector('button[iconname="thumb_up"]');
    }

    function cleanMarkdown(text) {
        if (!text) return '';
        return text
            .replace(/!\[.*?\]\(.*?\)/g, '') // 完成した画像マークダウンを除去
            .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
            .replace(/```[\s\S]*?```/g, '')
            .replace(/`[^`]+`/g, '')
            .replace(/(\*\*|__)(.*?)\1/g, '$2')
            .replace(/(\*|_)(.*?)\1/g, '$2')
            .replace(/^#+\s+/gm, '')
            .replace(/^[\-\*]{3,}$/gm, '')
            .replace(/^\s*[\-\*\+]\s+/gm, '')
            .replace(/^\s*\d+\.\s+/gm, '')
            .replace(/^>\s+/gm, '')
            .replace(/\n{3,}/g, '\n\n')
            .trim();
    }

    function splitIntoSentences(text) {
        return text.split(/(?<=[。!?\n])|(?<=\.\s)/g)
            .map(s => s.trim())
            .filter(s => s.length > 0);
    }

    // ===== VOICEVOX API (タイムアウト付き) =====
    function synthesizeSpeech(text, callback, customApiUrl = null, customSpeakerId = null, customSpeedScale = null, customVolumeScale = null) {
        const apiUrl = customApiUrl || config.apiUrl;
        const speakerId = customSpeakerId !== null ? customSpeakerId : config.speakerId;
        const speedScale = customSpeedScale !== null ? customSpeedScale : config.speedScale;
        const volumeScale = customVolumeScale !== null ? customVolumeScale : config.volumeScale;
        const queryUrl = `${apiUrl}/audio_query?text=${encodeURIComponent(text)}&speaker=${speakerId}`;

        GM_xmlhttpRequest({
            method: 'POST', url: queryUrl, timeout: 5000,
            onload: (response) => {
                if (response.status !== 200) return callback(null);
                try {
                    const audioQuery = JSON.parse(response.responseText);
                    audioQuery.speedScale = speedScale; audioQuery.volumeScale = volumeScale;
                    GM_xmlhttpRequest({
                        method: 'POST', url: `${apiUrl}/synthesis?speaker=${speakerId}`,
                        headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(audioQuery),
                        responseType: 'blob', timeout: 10000,
                        onload: (synthResponse) => callback(synthResponse.status === 200 ? URL.createObjectURL(synthResponse.response) : null),
                        onerror: () => callback(null), ontimeout: () => callback(null)
                    });
                } catch (e) { callback(null); }
            },
            onerror: () => callback(null), ontimeout: () => callback(null)
        });
    }

    // ===== 再生制御 =====
    function playNext() {
        if (state.shouldStop || state.isPlaying || state.audioQueue.length === 0) {
            if (!state.isSynthesizing && state.audioQueue.length === 0 && state.textQueue.length === 0) {
                state.isPlaying = false;
                updatePlayButton(false);
            }
            return;
        }
        state.isPlaying = true; updatePlayButton(true);
        const audioUrl = state.audioQueue.shift();
        const audio = new Audio(audioUrl);
        state.currentAudio = audio;
        audio.onended = audio.onerror = () => {
            URL.revokeObjectURL(audioUrl);
            state.isPlaying = false; state.currentAudio = null;
            if (!state.shouldStop) playNext();
        };
        audio.play().catch(() => { state.isPlaying = false; state.currentAudio = null; });
    }

    function processTextQueue() {
        if (state.shouldStop || state.isSynthesizing || state.textQueue.length === 0) return;
        state.isSynthesizing = true;
        const text = state.textQueue.shift();
        synthesizeSpeech(text, (audioUrl) => {
            state.isSynthesizing = false;
            if (state.shouldStop) { if (audioUrl) URL.revokeObjectURL(audioUrl); return; }
            if (audioUrl) { state.audioQueue.push(audioUrl); playNext(); }
            if (state.textQueue.length > 0) setTimeout(() => processTextQueue(), 100);
        });
    }

    function startPlayback(text) {
        stopPlayback();
        state.shouldStop = false;
        const cleanedText = cleanMarkdown(text);
        const sentences = splitIntoSentences(cleanedText);
        if (sentences.length === 0) return;
        state.textQueue = sentences;
        state.lastProcessedText = text;
        state.lastProcessedCleanText = cleanedText;
        updatePlayButton(true);
        processTextQueue();
    }

    function addToPlaybackQueue(newCleanText) {
        if (state.shouldStop || !newCleanText) return;
        const sentences = splitIntoSentences(newCleanText);
        if (sentences.length === 0) return;
        const wasIdle = !state.isPlaying && !state.isSynthesizing && state.textQueue.length === 0;
        state.textQueue.push(...sentences);
        if (wasIdle) { updatePlayButton(true); processTextQueue(); }
        else if (!state.isSynthesizing) { processTextQueue(); }
    }

    function stopPlayback() {
        state.shouldStop = true;
        if (state.currentAudio) {
            state.currentAudio.pause(); state.currentAudio.src = ''; state.currentAudio = null;
        }
        state.audioQueue.forEach(url => URL.revokeObjectURL(url));
        state.audioQueue = [];
        state.textQueue = [];
        state.textBuffer = '';
        state.isPlaying = false;
        state.isSynthesizing = false;
        updatePlayButton(false);
    }

    // ===== UI制御とメインロジック =====
    function updatePlayButton(isActive) {
        if (state.playButton) {
            state.playButton.textContent = isActive ? '■ 停止' : '🔊 再生';
        }
    }

    let debounceTimer = null;
    function handleUpdate() {
        if (debounceTimer) return;
        debounceTimer = setTimeout(() => {
            debounceTimer = null;
            try {
                const isCurrentlyGenerating = isGenerating();

                // 新しい回答の開始を検知(false -> true
                if (isCurrentlyGenerating && !state.wasGenerating) {
                    stopPlayback();
                    state.lastProcessedText = '';
                    state.lastProcessedCleanText = '';
                    state.shouldStop = false;
                }

                const currentText = getModelResponseText();
                if (config.autoPlay && !state.shouldStop && currentText) {
                    const cleanedFullText = cleanMarkdown(currentText);
                    // 差分があればバッファに追加
                    if (cleanedFullText.length > state.lastProcessedCleanText.length) {
                        const newCleanTextPart = cleanedFullText.substring(state.lastProcessedCleanText.length);
                        state.lastProcessedText = currentText;
                        state.lastProcessedCleanText = cleanedFullText;
                        state.textBuffer += newCleanTextPart;
                    }
                }

                // バッファから完成した文を切り出してキューに追加
                const sentenceRegex = /(?<=[。!?\n])/;
                if (sentenceRegex.test(state.textBuffer)) {
                    let parts = state.textBuffer.split(sentenceRegex);
                    const lastPart = parts[parts.length - 1] === '' ? '' : parts.pop();
                    const sentencesToQueue = parts.join('');

                    if (sentencesToQueue) {
                        addToPlaybackQueue(sentencesToQueue);
                    }
                    state.textBuffer = lastPart || '';
                }

                // 生成が完了した瞬間(true -> false)にバッファの残りを処理
                if (state.wasGenerating && !isCurrentlyGenerating) {
                    if (state.textBuffer.trim().length > 0) {
                        addToPlaybackQueue(state.textBuffer);
                        state.textBuffer = '';
                    }
                }
                state.wasGenerating = isCurrentlyGenerating;

                // 再生ボタンの追加/更新
                const lastTurn = document.querySelector('ms-chat-turn:last-of-type');
                if (lastTurn && !lastTurn.querySelector('#voicevox_play_button')) {
                    const footer = lastTurn.querySelector('.turn-footer');
                    if (footer) {
                        const button = document.createElement('button');
                        button.id = 'voicevox_play_button';
                        button.textContent = '🔊 再生';
                        button.style.cssText = `padding: 5px 12px; background: rgb(138, 180, 248); color: rgb(32, 33, 36); border: none; border-radius: 20px; cursor: pointer; margin-left: 8px; font-weight: 500;`;
                        button.onclick = () => {
                            const isActive = state.isPlaying || state.isSynthesizing || state.textQueue.length > 0;
                            if (isActive) { stopPlayback(); }
                            else { startPlayback(getModelResponseText()); }
                        };
                        footer.insertBefore(button, footer.firstChild);
                        state.playButton = button;
                    }
                }
                if (state.playButton) {
                    updatePlayButton(state.isPlaying || state.isSynthesizing || state.textQueue.length > 0);
                }

            } catch (e) {
                console.warn("AI Studio to VOICEVOX: handleUpdate error:", e);
            }
        }, 300);
    }

    // ===== DOM監視 =====
    const observer = new MutationObserver(handleUpdate);
    observer.observe(document.body, { childList: true, subtree: true });

    // ===== 設定画面(変更なし) =====
    let speakerList = [];

    function fetchSpeakers(callback) {
        GM_xmlhttpRequest({
            method: 'GET', url: `${config.apiUrl}/speakers`, timeout: 3000,
            onload: (response) => {
                if (response.status === 200) {
                    try { speakerList = JSON.parse(response.responseText); callback(true); }
                    catch (e) { callback(false); }
                } else { callback(false); }
            },
            onerror: () => callback(false), ontimeout: () => callback(false)
        });
    }

    function getSpeakerName(speakerId) {
        for (const speaker of speakerList) {
            for (const style of speaker.styles) {
                if (style.id === speakerId) return `${speaker.name} (${style.name})`;
            }
        }
        return `ID: ${speakerId} (不明)`;
    }

    function openSettings() {
        const overlay = document.createElement('div');
        overlay.style.cssText = `position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;`;
        const panel = document.createElement('div');
        panel.style.cssText = `background:#202124;color:#e8eaed;padding:24px;border-radius:8px;width:500px;max-height:90vh;overflow-y:auto;`;

        function createStyledElement(tag, style, textContent = '') {
            const el = document.createElement(tag);
            el.style.cssText = style;
            if (textContent) el.textContent = textContent;
            return el;
        }

        panel.appendChild(createStyledElement('h2', 'margin:0 0 20px;font-size:1.5em;', '🔊 VOICEVOX連携設定'));

        const apiUrlSection = createStyledElement('div', 'margin-bottom:16px;');
        apiUrlSection.appendChild(createStyledElement('label', 'display:block;margin-bottom:8px;color:#9aa0a6;', 'API URL:'));
        const apiUrlInput = createStyledElement('input', 'width:100%;padding:10px;background:#3c4043;color:#e8eaed;border:1px solid #5f6368;border-radius:4px;box-sizing:border-box;font-size:14px;');
        apiUrlInput.type = 'text'; apiUrlInput.value = config.apiUrl;
        apiUrlSection.appendChild(apiUrlInput); panel.appendChild(apiUrlSection);

        const speakerSection = createStyledElement('div', 'margin-bottom:16px;position:relative;');
        speakerSection.appendChild(createStyledElement('label', 'display:block;margin-bottom:8px;color:#9aa0a6;', 'スピーカー:'));
        const speakerInputContainer = createStyledElement('div', 'display:flex;gap:10px;align-items:center;');
        const speakerIdInput = createStyledElement('input', 'width:100px;padding:10px;background:#3c4043;color:#e8eaed;border:1px solid #5f6368;border-radius:4px;box-sizing:border-box;font-size:14px;');
        speakerIdInput.type = 'number'; speakerIdInput.value = config.speakerId;
        const speakerNameDisplay = createStyledElement('span', 'color:#81c995;font-weight:500;');
        const speakerListContainer = createStyledElement('div', 'position:absolute;top:100%;left:0;width:100%;z-index:10001;max-height:200px;overflow-y:auto;background:#3c4043;border:1px solid #5f6368;border-radius:4px;display:none;');
        speakerInputContainer.appendChild(speakerIdInput); speakerInputContainer.appendChild(speakerNameDisplay);
        speakerSection.appendChild(speakerInputContainer); speakerSection.appendChild(speakerListContainer);
        panel.appendChild(speakerSection);

        const speedSection = createStyledElement('div', 'margin-bottom:16px;');
        speedSection.appendChild(createStyledElement('label', 'display:block;margin-bottom:8px;color:#9aa0a6;', '読み上げ速度:'));
        const speedContainer = createStyledElement('div', 'display:flex;gap:12px;align-items:center;');
        const speedSlider = createStyledElement('input', 'flex:1;');
        speedSlider.type = 'range'; speedSlider.min = 0.5; speedSlider.max = 2.0; speedSlider.step = 0.1; speedSlider.value = config.speedScale;
        const speedValue = createStyledElement('span', 'min-width:50px;color:#8ab4f8;font-weight:500;text-align:right;', `${config.speedScale.toFixed(1)}x`);
        speedContainer.appendChild(speedSlider); speedContainer.appendChild(speedValue);
        speedSection.appendChild(speedContainer); panel.appendChild(speedSection);

        const volumeSection = createStyledElement('div', 'margin-bottom:16px;');
        volumeSection.appendChild(createStyledElement('label', 'display:block;margin-bottom:8px;color:#9aa0a6;', '音量:'));
        const volumeContainer = createStyledElement('div', 'display:flex;gap:12px;align-items:center;');
        const volumeSlider = createStyledElement('input', 'flex:1;');
        volumeSlider.type = 'range'; volumeSlider.min = 0.0; volumeSlider.max = 2.0; volumeSlider.step = 0.1; volumeSlider.value = config.volumeScale;
        const volumeValue = createStyledElement('span', 'min-width:50px;color:#8ab4f8;font-weight:500;text-align:right;', `${Math.round(config.volumeScale * 100)}%`);
        volumeContainer.appendChild(volumeSlider); volumeContainer.appendChild(volumeValue);
        volumeSection.appendChild(volumeContainer); panel.appendChild(volumeSection);

        const testSection = createStyledElement('div', 'margin:16px 0;padding-top:16px;border-top:1px solid #3c4043;');
        const testButton = createStyledElement('button', 'width:100%;padding:10px;background:#81c995;color:#202124;border:none;border-radius:4px;cursor:pointer;font-weight:500;', '🎵 サンプル音声を再生');
        testSection.appendChild(testButton); panel.appendChild(testSection);

        const autoplaySection = createStyledElement('div', 'margin-bottom:20px;padding-top:16px;border-top:1px solid #3c4043;');
        const autoplayLabel = createStyledElement('label', 'display:flex;align-items:center;cursor:pointer;');
        const autoplayCheckbox = createStyledElement('input', 'margin-right:12px;width:20px;height:20px;');
        autoplayCheckbox.type = 'checkbox'; autoplayCheckbox.checked = config.autoPlay;
        autoplayLabel.appendChild(autoplayCheckbox);
        autoplayLabel.appendChild(createStyledElement('span', '', '自動再生を有効にする'));
        autoplaySection.appendChild(autoplayLabel); panel.appendChild(autoplaySection);

        const actionsSection = createStyledElement('div', 'display:flex;justify-content:flex-end;gap:10px;padding-top:16px;border-top:1px solid #3c4043;');
        const cancelButton = createStyledElement('button', 'padding:10px 20px;background:transparent;color:#8ab4f8;border:1px solid #5f6368;border-radius:4px;cursor:pointer;', 'キャンセル');
        const saveButton = createStyledElement('button', 'padding:10px 20px;background:#8ab4f8;color:#202124;border:none;border-radius:4px;cursor:pointer;', '保存');
        actionsSection.appendChild(cancelButton); actionsSection.appendChild(saveButton);
        panel.appendChild(actionsSection);

        document.body.appendChild(overlay); overlay.appendChild(panel);

        let testAudio = null;
        const closePanel = () => { if (testAudio) { testAudio.pause(); } overlay.remove(); };
        speedSlider.oninput = () => speedValue.textContent = `${parseFloat(speedSlider.value).toFixed(1)}x`;
        volumeSlider.oninput = () => volumeValue.textContent = `${Math.round(parseFloat(volumeSlider.value) * 100)}%`;
        cancelButton.onclick = closePanel;
        overlay.onclick = (e) => { if (e.target === overlay) closePanel(); };
        document.addEventListener('click', (e) => { if (!speakerSection.contains(e.target)) speakerListContainer.style.display = 'none'; }, true);

        const updateSpeakerDisplay = () => {
            const id = parseInt(speakerIdInput.value);
            speakerNameDisplay.textContent = speakerList.length === 0 ? '取得中...' : getSpeakerName(id);
            speakerNameDisplay.style.color = speakerNameDisplay.textContent.includes('不明') ? '#f28b82' : '#81c995';
        };

        const populateSpeakerList = () => {
            if (speakerList.length === 0) { speakerListContainer.innerHTML = `<div style="padding:10px;color:#9aa0a6;">スピーカー情報なし</div>`; return; }
            speakerListContainer.innerHTML = speakerList.map(sp => sp.styles.map(st =>
                `<div data-id="${st.id}" style="padding:10px;cursor:pointer;border-bottom:1px solid #5f6368;"><div>${sp.name} (${st.name})</div></div>`
            ).join('')).join('');
            speakerListContainer.querySelectorAll('[data-id]').forEach(item => {
                item.onclick = () => { speakerIdInput.value = item.dataset.id; updateSpeakerDisplay(); speakerListContainer.style.display = 'none'; };
                item.onmouseenter = () => item.style.backgroundColor = '#5f6368';
                item.onmouseleave = () => item.style.backgroundColor = '';
            });
        };

        const refreshSpeakers = () => {
            speakerList = []; updateSpeakerDisplay();
            fetchSpeakers(success => {
                if (success) { populateSpeakerList(); }
                else { speakerNameDisplay.textContent = 'API接続エラー'; speakerNameDisplay.style.color = '#f28b82'; }
                updateSpeakerDisplay();
            });
        };

        speakerIdInput.onfocus = () => speakerListContainer.style.display = 'block';
        speakerIdInput.oninput = updateSpeakerDisplay;
        apiUrlInput.onchange = refreshSpeakers;
        testButton.onclick = () => {
            if (testAudio && !testAudio.paused) { testAudio.pause(); testButton.textContent = '🎵 サンプル音声を再生'; return; }
            testButton.textContent = '合成中...'; testButton.disabled = true;
            synthesizeSpeech('こんにちは。これはサンプル音声です。', url => {
                testButton.disabled = false;
                if (url) {
                    testAudio = new Audio(url); testAudio.play(); testButton.textContent = '再生中... ■停止';
                    testAudio.onended = () => { URL.revokeObjectURL(url); testButton.textContent = '🎵 サンプル音声を再生'; };
                } else { testButton.textContent = '再生失敗'; }
            }, apiUrlInput.value, parseInt(speakerIdInput.value), parseFloat(speedSlider.value), parseFloat(volumeSlider.value));
        };
        saveButton.onclick = () => {
            config.apiUrl = apiUrlInput.value.trim();
            config.speakerId = parseInt(speakerIdInput.value);
            config.autoPlay = autoplayCheckbox.checked;
            config.speedScale = parseFloat(speedSlider.value);
            config.volumeScale = parseFloat(volumeSlider.value);
            GM_setValue(CONFIG_KEY, config);
            closePanel();
        };
        refreshSpeakers();
    }

    // ===== 初期化 =====
    GM_registerMenuCommand('VOICEVOX設定', openSettings);
    setTimeout(handleUpdate, 2000);

})();

AI Studio VOICEVOX Reader 設定マニュアル

このマニュアルでは、AI Studioの回答をVOICEVOXで読み上げるためのスクリプト設定について説明します。
(何も設定しなくてもご利用になれます)

1. 設定画面の開き方

この設定は、AI Studioのページ上ではなく、ブラウザのTampermonkey拡張機能メニューから開きます。

  1. ブラウザの右上にあるTampermonkeyのアイコンをクリックします。
  2. 表示されたメニューの中から「VOICEVOX設定」という項目をクリックしてください。
    C

クリックすると、画面の中央に設定パネルが表示されます。

2. 各項目の説明

設定パネルには以下の項目があります。

【1】API URL

  • 概要:
    あなたのPCで起動しているVOICEVOX ENGINEの場所(アドレス)を指定します。
  • 詳細:
    通常、VOICEVOXを自分のPCで起動している場合、この値は http://localhost:50021 のままで問題ありません。変更する必要はほとんどありません。
    もし異なるPCや設定でVOICEVOX ENGINEを動かしている場合のみ、そのアドレスに合わせて変更してください。

【2】スピーカー

  • 概要:
    読み上げに使用するVOICEVOXのキャラクター(話者)とスタイルを選択します。
  • 使い方:
    1. 入力欄をクリックすると、現在利用可能なすべてのスピーカーとスタイルが一覧で表示されます。
    2. 一覧から使用したいキャラクター名とスタイル名(例: 四国めたん (ノーマル))をクリックして選択します。
    3. 選択すると、自動的にIDが入力され、右側に緑色で名前が表示されます。
  • 補足:
    • 緑色の文字: スピーカーが正常に認識されています。
    • 赤色の文字: そのIDのスピーカーが見つかりません。IDが間違っているか、API URLへの接続に失敗しています。

【3】サンプル音声を再生

  • 概要:
    現在のAPI URLとスピーカーIDの設定が正しいか、テストするためのボタンです。
  • 使い方:
    1. API URLとスピーカーを設定した後、このボタンをクリックします。
    2. 「こんにちは。これはサンプル音声です。」という音声が再生されれば、設定は成功です。
  • もし再生されない場合:
    • PCでVOICEVOXが起動しているか確認してください。
    • API URLが正しいか確認してください。
    • スピーカーIDが有効なものか確認してください。

【4】自動再生を有効にする

  • 概要:
    AI Studioが回答を生成し終えた後、自動的に読み上げを開始するかどうかを設定します。
  • 使い方:
    • チェックを入れる (有効): 回答が完了すると、自動で読み上げが始まります。
    • チェックを外す (無効): 自動では読み上げません。回答の下に表示される「🔊 再生」ボタンを手動でクリックした場合のみ読み上げます。

【5】キャンセル / 保存 ボタン

  • キャンセル:
    ここで行った変更を一切保存せずに設定画面を閉じます。
  • 保存:
    ここで行った変更を保存して、設定をスクリプトに反映させます。設定を変更した場合は、必ずこのボタンを押してください。

3. おすすめの設定の流れ

初めて設定を行う際は、以下の手順で行うとスムーズです。

  1. PCでVOICEVOXアプリケーションを起動しておきます。
  2. Tampermonkeyメニューから「VOICEVOX設定」を開きます。
  3. 「API URL」が http://localhost:50021 になっていることを確認します。
  4. 「スピーカー」の入力欄をクリックし、お好みのキャラクターを選択します。
  5. 🎵 サンプル音声を再生」ボタンを押し、音声が聞こえることを確認します。
  6. 「自動再生」のオン/オフを好みに合わせて設定します。
  7. 最後に「保存」ボタンを押して設定を完了します。

以上で設定は完了です。快適で"健全"な読み上げをお楽しみください。

個人の感想

もち子さん(ノーマル) デフォルト設定。もはやこれで十分
猫使ビィ(ノーマル) □リ声だがライトに
猫使ビィ(人見知り) □リ声だがウェットに
四国めたん(ささやき) まさにASMR?!
四国めたん(ヒソヒソ) ささやきよりマイルド
ずんだもん(ノーマル) ネタ
ずんだもん(ささやき) ……あれ、意外と

あとナースロボ_タイプTも内緒話があってよい。

あくまで健全な使用をお願いします!

結論から言うと、ずんだもんやVOICEVOXの音声で「エロ音声」を作って公開すると、

主に「法律上のリスク」と「規約違反によるペナルティ」の2種類のリスクがありえます!!

1. 法律上の話(刑事罰になるパターン)

これは「ずんだもん/VOICEVOXだから」というより、「中身が違法かどうか」で決まる部分。

  • 成人向けの表現でも、露骨でわいせつと評価される音声を不特定多数に配布・販売すると、「わいせつ電磁的記録等頒布罪」(刑法175条)に当たる可能性がある。公開・販売する側が対象で、視聴する側は通常この罪には当たらない。
  • 未成年を性的対象にした内容(年齢を明示していないが明らかに児童と認識されるキャラなど)になると、「児童ポルノ禁止法」絡みで、制作・頒布はもちろん、ダウンロードや所持も処罰対象になり得る。
  • さらに、違法アップロードされた有料音声をダウンロードすると、著作権法上の違法ダウンロードとして別途罰則の対象になり得る。

2. VOICEVOX本体の利用規約

  • VOICEVOX自体は「商用・非商用問わず利用可能」だが、「作成された音声は各キャラごとの音声ライブラリの規約に従うこと」とされている。
  • VOICEVOX関連の解説サイトでは、「公式ガイドラインでR-18コンテンツ(エロ/グロ)は禁止」と明記されているとされ、アダルト用途での利用は規約違反になると整理されている。
  • つまり「ソフトとしては使えるけど、R-18用途はライブラリ(キャラ)の規約でNG」という形なので、エロ音声用途は“利用停止・削除要請などのリスク”がある。

3. ずんだもん・東北ずん子系ガイドライン

  • ずんだもん周りは、「東北ずん子ずんだもんプロジェクト」のキャラクター利用ガイドラインや、「ZUNDAMON Major Debut EP 二次創作ガイドライン」などがあり、公序良俗に反する利用やイメージを大きく毀損する利用を禁止している。
  • ピクシブ百科などの解説でも、ずんだもんの過度な暴力・政治・陰謀論利用等はガイドライン違反のリスクが高いとされ、性的な二次創作も「運営に見つかればNGになり得る」というスタンスが紹介されている。
  • 公式が「R-18全面OK」としているわけではなく、“健全な利用前提”でガイドラインが組まれているため、露骨なエロ音声公開は「キャラ規約違反→削除要請・使用禁止要請」という方向のリスクがある。

4. 「個人で聞く」場合と「公開する」場合

  • 自分のPC内でこっそりテキストを入れて出力させ、自分だけで聞くぶんについては、通常は「規約違反かどうか」は運営に把握されにくく、刑事罰にも直結しない。
  • しかし、その音声を
    • 動画サイトや音声配信サイトで公開する
    • 有料で販売する
    • 不特定多数に配布する
      となると、
    • VOICEVOX・キャラ側の利用規約違反
    • 場合によってはわいせつ物頒布罪・児童ポルノ関連法違反
      になり得るので、ここから先はかなりグレー~アウトゾーンになる。

5. 実際にあり得るペナルティ

  • 規約違反レベル
    • 作品削除要請
    • 関連サービスでのアカウントBAN
    • キャラクター・音源の使用停止要請
      などの「民間サービス上のペナルティ」が中心。
  • 法律レベル
    • わいせつ物頒布等罪での摘発(2年以下の拘禁刑や罰金など)。
    • 児童ポルノ関連での摘発(制作・頒布・所持ともに重い刑罰)。
    • 著作権法違反(違法ダウンロードや無断販売等)。

6. 実務的な注意点(グレーに踏み込みたくない場合)

  • VOICEVOX・ずんだもんでの性表現は避ける(VOICEVOX関連の有志まとめでも「R18利用はNG」と整理されている)。
  • どうしても大人向け音声を作りたい場合は、最初からR18利用を許可している別の音源や、自前で収録した声を使う。
  • 公開する場合は、
    • 成人向けであることを明記し
    • 18歳未満がアクセスできない設定にし
    • キャラや音源の利用規約をあらためて確認する
      ことが重要になる。

7. 法律相談について

  • ここでの説明は一般的な情報であり、具体的に「この作品はセーフかアウトか」を判定することはできない。
  • 不安な場合は、IT・著作権・刑事事件に詳しい弁護士に、具体的な内容(台本・公開方法・対象サービスなど)を見せた上で相談するのが安全。

ざっくり言うと、「ずんだもんやVOICEVOXでエロ音声を作ってネットに公開するのは、規約的にはほぼNGで、内容次第では法律的にもアウトになり得る」という感じ。

その他のツール

Google AI Studio に直接画像を出したい!

通称:MIR(スタ画)
https://rentry.co/3bnuvgwu

Wikiのプロンプトまとめ等をみながら、プロンプトだけを即コピーしたい人用に作成しました

通称:即コピ
https://rentry.co/8772bcnh

Edit

Pub: 08 Nov 2025 17:12 UTC

Edit: 18 Nov 2025 12:12 UTC

Views: 369