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="閉じる">×</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>→</span>
<span class="gpt-replace-to">${escapeHTML(r.to)}</span>
<button class="gpt-replace-remove-btn" data-index="${index}" title="削除">×</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) が読み込まれました。");
})();
|