ChatGPT 単語の置き換え

あくまでも見た目だけ置き換わってます ※当然AI側は置換前の単語を認識
赤消し対策に役立ててください
Tampermonkeyって拡張機能につっこんでください
@matchを変えれば他のサイトでも使えます

追記 

置換前後は一文字が軽くて反映が早いので推奨 s→妹 のように sis→妹でも良いが複数文字の置換は反映が重くなりがち ギリシャ文字とかがおすすめかも

追記 動作しなくなった件についての書き込み[★76 >>89]

GPTの応答文生成アニメーションは1文字もしくは1単語(そのまんまトークン単位かも)ごとに<span>タグで囲まれて分割して生成されていて、これはすでにアニメーションが終わって出力済みのログでも律儀に保持されてる。

GPTの仕様変更かChromeのアプデによる挙動の変更かはわからないけど、ここ数日でそもそもの表示形式かもしくは分割の細かさが変わった。

例えば置換前の単語が「テスト」だった場合、これが「 / テスト」とか「テス / ト」のように分割されてしまうと、画面では「テスト」と表示されていても内部では2~3個の独立した文字列という扱いだからスクリプトは置換対象として取れない。

(実際にログ出力するコードを追記してコンソールで確認したのでハルシではない。ユーザーが送信した文は置換できてGPTが生成した文だけが置換できないのはこれが理由)

これに対応するスクリプトを実装するのは専門知識のない俺には無理なので、応急処置として置換したい単語を🝖とか🜲みたいな絶対に自然文で生成されなさそうな1文字で出力するようにプロンプトを調整した。

作者氏がrentryで推奨してた「s→妹」のような1文字置換を守ってる人は問題ないはずだけど、俺は誤検出恐れて[]で囲んで2文字以上にしちゃってたので症状出たっていう、まあ自業自得でした。

もし原因がChromeのアプデによる挙動変更だとしたら、ユーザー間で時間差のあるアプデが適用されていくにつれて同じ症状に悩む人が出てくるかもしれないし、
ちゃんとコード書ける人の修正や改善の一助にもなればと思ったので、長文で鬱陶しいだろうけど一応レスとして残しておく。

  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
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
// ==UserScript==
// @name         ChatGPT 動的単語置換 (複数対応・UI版)
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  ChatGPTのインターフェース上の指定した複数の単語を指定した別の単語に置き換えます。設定はページ内のUIから変更可能
// @author       You (Modified by Assistant)
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 設定値の管理 ---
    const KEY_REPLACEMENTS = 'chatgptReplacements_v3'; // 保存キーを変更

    // 設定値を読み込む (デフォルト値を設定)
    // 形式: [{ from: '置換前1', to: '置換後1', enabled: true }, { from: '置換前2', to: '置換後2', enabled: false }]
    let replacements = [];
    try {
        replacements = JSON.parse(GM_getValue(KEY_REPLACEMENTS, JSON.stringify([
            { from: 'User', to: 'ママ', enabled: true },
            { from: 'ChatGPT', to: 'アシスタント', enabled: true }
        ])));
        // 互換性のため、enabledがない場合はtrueを追加
        replacements = replacements.map(r => ({ ...r, enabled: r.enabled !== undefined ? r.enabled : true }));
    } catch (e) {
        console.error("置換設定の読み込みに失敗しました:", e);
        replacements = [
            { from: 'User', to: 'ママ', enabled: true },
            { from: 'ChatGPT', to: 'アシスタント', enabled: true }
        ];
        GM_setValue(KEY_REPLACEMENTS, JSON.stringify(replacements)); // エラー時はデフォルトで上書き
    }

    // --- 正規表現の特殊文字をエスケープする関数 ---
    function escapeRegExp(string) {
        // $& は一致した部分文字列全体を意味します
        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    // --- 置換処理 ---
    let currentRegexes = []; // 現在の置換用正規表現と置換後文字列のペアをキャッシュ

    const updateRegexes = () => {
        currentRegexes = replacements
            .filter(r => r.enabled && r.from) // 有効かつ置換元が空でないものだけ
            .map(r => {
                try {
                    const escapedFrom = escapeRegExp(r.from);
                    // 大文字小文字を区別せず、グローバルに置換する正規表現を作成
                    return { regex: new RegExp(escapedFrom, 'gi'), to: r.to };
                } catch (e) {
                    console.error(`正規表現の作成に失敗しました (from: ${r.from}):`, e);
                    return null; // エラーが発生した場合はnullを返す
                }
            })
            .filter(r => r !== null); // エラーが発生したものを除外
    };
    updateRegexes(); // 初期化

    const performReplace = (node) => {
        if (!currentRegexes.length) {
            return; // 置換ルールがなければ何もしない
        }

        // テキストノードのみを処理対象とする
        if (node.nodeType === Node.TEXT_NODE) {
            let replacedText = node.textContent;
            let textChanged = false;

            // 有効な各正規表現で順番に置換を試みる
            currentRegexes.forEach(({ regex, to }) => {
                // testメソッドでチェックしてからreplaceを実行
                if (regex.test(replacedText)) {
                    const beforeReplace = replacedText;
                    replacedText = replacedText.replace(regex, to);
                    if (beforeReplace !== replacedText) {
                        textChanged = true;
                    }
                    regex.lastIndex = 0; // グローバル検索の状態をリセット
                }
            });

            // テキストが実際に変更された場合のみDOMを更新
            if (textChanged && node.textContent !== replacedText) {
                 // 親が設定UIや特定の要素でないことを確認(オプション)
                if (!node.parentNode || !node.parentNode.closest('#gpt-replace-settings-panel')) {
                    node.textContent = replacedText;
                }
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            // 要素ノードの場合は、その子ノードに対して再帰的に処理
            // ただし、SCRIPT, STYLE, TEXTAREA, INPUT, 設定パネルの中は処理しない
            const tagName = node.nodeName.toUpperCase();
            const isEditable = node.isContentEditable; // contenteditable属性をチェック
            const isInSettingsPanel = node.closest('#gpt-replace-settings-panel'); // 設定パネル内かチェック

            if (tagName !== 'SCRIPT' && tagName !== 'STYLE' && tagName !== 'TEXTAREA' && tagName !== 'INPUT' && !isEditable && !isInSettingsPanel) {
                // NodeListではなくArrayに変換してからforEachを使う方が安全な場合がある
                Array.from(node.childNodes).forEach(child => performReplace(child));
            }
        }
    };


    // --- MutationObserver ---
    const targetNode = document.body;
    // characterData: true を含めると、テキスト編集中のカーソル移動でも発火することがあるため、
    // childList  subtree のみに絞り、追加されたノード全体をチェックする方が安定する場合がある
    const config = { childList: true, subtree: true };

    let observer = null; // オブザーバーのインスタンスを保持

    const callback = function(mutationsList, obs) {
        // 変更を一時的に無効化 (置換処理による無限ループを防ぐ)
        // disconnect/observe を繰り返すと重くなる可能性があるので注意
        // 代わりにフラグ管理なども検討できるが、まずはシンプルな方法で実装
        obs.disconnect();

        for(const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => {
                    performReplace(node);
                });
                // 削除されたノードは無視
            }
            // characterDataの監視をやめたので、以下のブロックは不要
            // else if (mutation.type === 'characterData') {
            //     performReplace(mutation.target);
            // }
        }

        // 監視を再開
        // disconnectした場合は再度observeする必要がある
        if (observer) { // observerがnullでないことを確認
             observer.observe(targetNode, config);
        }
    };

    // --- 初期実行と監視開始 ---
    const startObserver = () => {
        if (observer) {
            observer.disconnect(); // 既存のオブザーバーがあれば停止
        }
        // 監視開始前に一度ページ全体を置換
        performReplace(targetNode);
        // 新しいオブザーバーを作成して監視を開始
        observer = new MutationObserver(callback);
        observer.observe(targetNode, config);
    };

    // --- 設定UI ---
    let settingsPanel = null;
    let isPanelVisible = false;

    function createSettingsPanel() {
        if (settingsPanel) {
            settingsPanel.style.display = 'block';
            isPanelVisible = true;
            renderReplacementsList(); // 表示時にリストを再描画
            return;
        }

        settingsPanel = document.createElement('div');
        settingsPanel.id = 'gpt-replace-settings-panel';
        settingsPanel.innerHTML = `
            <div class="gpt-replace-settings-header">
                <h3>単語置換設定</h3>
                <button id="gpt-replace-close-btn" title="閉じる">&times;</button>
            </div>
            <div id="gpt-replace-list"></div>
            <div class="gpt-replace-add-form">
                <h4>新しい置換ルールを追加</h4>
                <input type="text" id="gpt-replace-from-input" placeholder="置換前の単語">
                <input type="text" id="gpt-replace-to-input" placeholder="置換後の単語">
                <button id="gpt-replace-add-btn">追加</button>
            </div>
            <div class="gpt-replace-footer">
                <button id="gpt-replace-save-btn">保存して再読込</button>
                 <button id="gpt-replace-cancel-btn">キャンセル</button>
            </div>
        `;
        document.body.appendChild(settingsPanel);
        isPanelVisible = true;

        // イベントリスナーを設定
        document.getElementById('gpt-replace-close-btn').addEventListener('click', hideSettingsPanel);
        document.getElementById('gpt-replace-cancel-btn').addEventListener('click', hideSettingsPanel);
        document.getElementById('gpt-replace-add-btn').addEventListener('click', addReplacement);
        document.getElementById('gpt-replace-save-btn').addEventListener('click', saveReplacements);

        // リスト部分のイベントリスナー(イベント委任)
        document.getElementById('gpt-replace-list').addEventListener('click', handleListClick);
        document.getElementById('gpt-replace-list').addEventListener('change', handleListChange); // チェックボックス用

        renderReplacementsList(); // 初期リスト表示
    }

     function handleListClick(event) {
        if (event.target.classList.contains('gpt-replace-remove-btn')) {
            const index = parseInt(event.target.dataset.index, 10);
            removeReplacement(index);
        }
    }

     function handleListChange(event) {
        if (event.target.classList.contains('gpt-replace-enabled-checkbox')) {
            const index = parseInt(event.target.dataset.index, 10);
            toggleReplacementEnabled(index, event.target.checked);
        }
    }


    function renderReplacementsList() {
        if (!settingsPanel) return;
        const listDiv = document.getElementById('gpt-replace-list');
        listDiv.innerHTML = ''; // リストをクリア

        if (replacements.length === 0) {
            listDiv.innerHTML = '<p>置換ルールがありません。</p>';
            return;
        }

        replacements.forEach((r, index) => {
            const itemDiv = document.createElement('div');
            itemDiv.className = 'gpt-replace-item';
            itemDiv.innerHTML = `
                <input type="checkbox" class="gpt-replace-enabled-checkbox" data-index="${index}" ${r.enabled ? 'checked' : ''} title="有効/無効">
                <span class="gpt-replace-from">${escapeHTML(r.from)}</span>
                <span>&rarr;</span>
                <span class="gpt-replace-to">${escapeHTML(r.to)}</span>
                <button class="gpt-replace-remove-btn" data-index="${index}" title="削除">&times;</button>
            `;
            listDiv.appendChild(itemDiv);
        });
    }

    // HTMLエスケープ関数
    function escapeHTML(str) {
        const p = document.createElement('p');
        p.textContent = str;
        return p.innerHTML;
    }


    function addReplacement() {
        const fromInput = document.getElementById('gpt-replace-from-input');
        const toInput = document.getElementById('gpt-replace-to-input');
        const from = fromInput.value.trim();
        const to = toInput.value.trim(); // toは空でも許可するかもしれないが、一旦trim

        if (from) { // fromが空でなければ追加
            replacements.push({ from, to, enabled: true });
            fromInput.value = '';
            toInput.value = '';
            renderReplacementsList(); // リストを更新
        } else {
            alert('「置換前の単語」を入力してください。');
        }
    }

    function removeReplacement(index) {
        if (index >= 0 && index < replacements.length) {
            replacements.splice(index, 1);
            renderReplacementsList(); // リストを更新
        }
    }

     function toggleReplacementEnabled(index, isEnabled) {
        if (index >= 0 && index < replacements.length) {
            replacements[index].enabled = isEnabled;
            // 保存は「保存ボタン」で行うので、ここではリスト再描画は不要(チェックボックスの状態は変わる)
            // renderReplacementsList();
        }
    }

    function saveReplacements() {
        try {
            GM_setValue(KEY_REPLACEMENTS, JSON.stringify(replacements));
            updateRegexes(); // 正規表現キャッシュを更新
            alert('設定を保存しました。ページ全体を再チェックします。');
            hideSettingsPanel();

            // 設定変更を即時反映させるためにページ全体を再チェック
            // disconnect/observe を行う
             if (observer) {
                observer.disconnect();
            }
            performReplace(document.body); // 全体置換を実行
             if (observer) { // 再度監視を開始
                 observer.observe(targetNode, config);
             }

        } catch (e) {
            console.error("設定の保存に失敗しました:", e);
            alert('設定の保存中にエラーが発生しました。');
        }
    }

    function hideSettingsPanel() {
        if (settingsPanel) {
            // 保存せずに閉じる場合、変更前の状態に戻す
            try {
                replacements = JSON.parse(GM_getValue(KEY_REPLACEMENTS, '[]'));
                 replacements = replacements.map(r => ({ ...r, enabled: r.enabled !== undefined ? r.enabled : true }));
            } catch (e) {
                 console.error("キャンセル時の設定再読み込みに失敗:", e);
                 // エラーが発生した場合のフォールバック(空にするか、デフォルトに戻すか)
                 replacements = [];
            }
            settingsPanel.style.display = 'none';
            isPanelVisible = false;
        }
    }

    // --- スタイル設定 ---
    GM_addStyle(`
        #gpt-replace-settings-panel {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 350px;
            max-height: 80vh; /* 高さを制限 */
            background-color: #f9f9f9;
            border: 1px solid #ccc;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            z-index: 9999;
            font-family: sans-serif;
            font-size: 14px;
            color: #333;
            display: none; /* 初期状態は非表示 */
            flex-direction: column; /* 縦方向に要素を配置 */
        }
        #gpt-replace-settings-panel.visible {
             display: flex; /* 表示時はflexに */
        }
        .gpt-replace-settings-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 15px;
            background-color: #eee;
            border-bottom: 1px solid #ccc;
            border-top-left-radius: 8px;
            border-top-right-radius: 8px;
        }
        .gpt-replace-settings-header h3 {
            margin: 0;
            font-size: 16px;
        }
        #gpt-replace-close-btn {
            background: none;
            border: none;
            font-size: 20px;
            cursor: pointer;
            color: #666;
            padding: 0 5px;
        }
         #gpt-replace-close-btn:hover {
             color: #000;
         }
        #gpt-replace-list {
            padding: 10px 15px;
            overflow-y: auto; /* 内容が多い場合にスクロール */
            flex-grow: 1; /* 残りの高さを埋める */
            min-height: 100px; /* 最小高さを確保 */
        }
        .gpt-replace-item {
            display: flex;
            align-items: center;
            padding: 8px 0;
            border-bottom: 1px solid #eee;
        }
         .gpt-replace-item:last-child {
             border-bottom: none;
         }
         .gpt-replace-item input[type="checkbox"] {
             margin-right: 8px;
             cursor: pointer;
         }
        .gpt-replace-item span {
            margin: 0 5px;
            word-break: break-all; /* 長い単語がはみ出ないように */
        }
         .gpt-replace-item .gpt-replace-from {
             font-weight: bold;
             color: #007bff; /* 置換前を青色に */
         }
         .gpt-replace-item .gpt-replace-to {
              font-weight: bold;
              color: #28a745; /* 置換後を緑色に */
         }
        .gpt-replace-remove-btn {
            background: none;
            border: 1px solid #ddd;
            color: #dc3545;
            cursor: pointer;
            font-size: 14px;
            line-height: 1;
            padding: 2px 5px;
            border-radius: 4px;
            margin-left: auto; /* 右端に寄せる */
        }
        .gpt-replace-remove-btn:hover {
            background-color: #dc3545;
            color: white;
        }
        .gpt-replace-add-form {
            padding: 15px;
            border-top: 1px solid #ccc;
            background-color: #f0f0f0;
        }
         .gpt-replace-add-form h4 {
             margin-top: 0;
             margin-bottom: 10px;
             font-size: 14px;
         }
        .gpt-replace-add-form input[type="text"] {
            width: calc(50% - 30px); /* ボタン幅を考慮 */
            padding: 8px;
            margin-right: 5px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 13px;
        }
        .gpt-replace-add-form button {
            padding: 8px 12px;
            border: none;
            background-color: #007bff;
            color: white;
            border-radius: 4px;
            cursor: pointer;
            font-size: 13px;
        }
        .gpt-replace-add-form button:hover {
            background-color: #0056b3;
        }
        .gpt-replace-footer {
            padding: 10px 15px;
            border-top: 1px solid #ccc;
            background-color: #eee;
            text-align: right;
            border-bottom-left-radius: 8px;
            border-bottom-right-radius: 8px;
        }
        .gpt-replace-footer button {
            padding: 8px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-left: 10px;
            font-size: 14px;
        }
        #gpt-replace-save-btn {
            background-color: #28a745;
            color: white;
        }
        #gpt-replace-save-btn:hover {
            background-color: #218838;
        }
         #gpt-replace-cancel-btn {
             background-color: #6c757d;
             color: white;
         }
         #gpt-replace-cancel-btn:hover {
             background-color: #5a6268;
         }
    `);

    // --- Tampermonkey メニュー設定 ---
    GM_registerMenuCommand('置換設定を開く/閉じる', () => {
        if (isPanelVisible) {
            hideSettingsPanel();
        } else {
            // パネルが存在しないか非表示なら作成または表示
            if (!settingsPanel) {
                createSettingsPanel();
                 settingsPanel.classList.add('visible'); // 表示クラスを追加
            } else {
                settingsPanel.style.display = 'flex'; // 再表示
                 settingsPanel.classList.add('visible');
                 isPanelVisible = true;
                 renderReplacementsList(); // 再表示時にリストを更新
            }
        }
    });

    // --- 初期化処理 ---
    // ページ読み込み完了後に実行されることを保証(より安全)
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startObserver);
    } else {
        startObserver(); // すでに読み込み完了している場合
    }

    console.log("ChatGPT 動的単語置換スクリプト (v3.0) が読み込まれました。");

})();
Edit Report
Pub: 26 Apr 2025 10:31 UTC
Edit: 11 May 2025 16:50 UTC
Views: 755