入り浸り雑談slack

昼間に雑談をするためだけの雑談slackのことや、私の思うことなどを書きます。

ChatGPTやGeminiの強調表示が*そのまま*になる問題を防ぐTampermonkeyスクリプトを作った

トップ画像 どうもみなさん、サメジ部長です。

ChatGPTやGeminiを日常的に使っていると、AIがMarkdown形式で出力したテキストの強調表示が、うまくHTMLに変換されずに **こんな感じ***こんな感じ*アスタリスクがそのまま表示されてしまうこと、ありませんか? そして、そのままスタイルミスをコピペしているクソブログも見たことがありませんか?

例えばこういうような。

Geminiで起きた強調表示ミス
Geminiで起きた強調表示ミス

今回は、この地味ながらも気になる問題を解決するために私とAIで作ったTampermonkeyスクリプトを紹介します。導入するとこのように修正できますよ。

対応スクリプトによる適用後
対応スクリプトによる対応後

なぜこの問題が起きるのか?

この記事はほぼGeminiに書かせています。Geminiが言うには、以下のような理由で現象が起こるようです。

ChatGPTやGeminiのようなモダンなWebアプリケーションは、ページ全体を再読み込みすることなく、動的にコンテンツを生成・更新します。AIからの返信も、リアルタイムでページに挿入されていきます。

この時、Markdownの記法(***)をリッチなHTML(<strong><em>)に変換する処理が追いつかなかったり、特定の条件下でうまく機能しなかったりすることが原因で、記号がそのまま表示されてしまう現象が起きます。

と、いうことでTampermonkeyというブラウザにJavaScriptベースのカスタムスクリプト実行プラットフォームを使って、AIのスタイルミスを修正するスクリプトを書いてみましょう。

導入方法

  1. お使いのブラウザに Tampermonkey 拡張機能をインストールします。
  2. Tampermonkeyの管理画面を開き、「新規スクリプトを追加」をクリックします。
    Tempermonkey設定画面
    Tempermonkey設定画面
  3. 表示されたエディタに、コードをすべて貼り付けます。
  4. ファイルメニューから保存をクリックします。
    ファイルメニューから保存を選択してください。
    ファイルメニュー

これで設定は完了しました。ChatGPTやGeminiのページを開けば、スクリプトが自動で有効になり、強調表示が正しくレンダリングされるようになります。

ブラウザのプラグインアイコンがこんな風になれば動作しています。

Tempermonkeyアイコン
Tempermonkeyアイコン

完成したTampermonkeyスクリプト

以下、ChatGPTとGeminiに対応したスタイル修正スクリプトです。

// ==UserScript==
// @name         Enhanced Markdown Bold/Italic Fixer
// @namespace    http://tampermonkey.net/
// @version      7.0
// @description  Convert **bold** and *italic* markdown to HTML tags on ChatGPT and Gemini with infinite loop prevention
// @author       Merged Enhanced Version
// @match        https://chatgpt.com/*
// @match        https://gemini.google.com/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  // Configuration
  const SKIP_ELEMENTS = "pre, code, kbd, samp, textarea, script, style";
  const PROCESSED_MARK = 'data-markdown-fixer-processed';
  const DEBUG = false;

  // Enhanced regex patterns from chatgpt.js
  const RX_BOLD = /\*\*([\s\S]+?)\*\*/g;
  const RX_ITALIC = /(^|[^*])\*([^*\s][\s\S]*?)\*(?=[^*]|$)/g;

  // Safe HTML conversion with loop prevention
  const htmlConvert = (html) => {
    return html
      .replace(RX_BOLD, '<strong class="em-fix-bold">$1</strong>')
      .replace(RX_ITALIC, (_, p1, p2) => p1 + '<em class="em-fix-italic">' + p2 + "</em>");
  };

  // Fragment-based safe text conversion (from gemini.js approach)
  const convertTextNodeSafely = (node) => {
    if (!node.parentElement ||
        node.parentElement.closest(SKIP_ELEMENTS) ||
        node.parentElement.hasAttribute(PROCESSED_MARK)) {
      return;
    }

    const text = node.textContent;
    if (!text.includes('*')) return;

    const parent = node.parentElement;
    if (!parent) return;

    const fragment = document.createDocumentFragment();
    let lastIndex = 0;
    let hasChanges = false;

    // Process bold patterns
    let workingText = text;
    const boldMatches = [...workingText.matchAll(RX_BOLD)];

    if (boldMatches.length > 0) {
      hasChanges = true;
      lastIndex = 0;

      boldMatches.forEach(match => {
        // Add text before match
        if (match.index > lastIndex) {
          fragment.appendChild(document.createTextNode(workingText.slice(lastIndex, match.index)));
        }

        // Add bold element
        const strong = document.createElement('strong');
        strong.className = 'em-fix-bold';
        strong.textContent = match[1];
        fragment.appendChild(strong);

        lastIndex = match.index + match[0].length;
      });

      // Add remaining text
      if (lastIndex < workingText.length) {
        fragment.appendChild(document.createTextNode(workingText.slice(lastIndex)));
      }

      // Update working text for italic processing
      workingText = fragment.textContent;
    }

    // Process italic patterns on remaining text
    if (workingText.includes('*')) {
      const italicMatches = [...workingText.matchAll(RX_ITALIC)];

      if (italicMatches.length > 0) {
        hasChanges = true;
        const newFragment = document.createDocumentFragment();
        lastIndex = 0;

        italicMatches.forEach(match => {
          // Add text before match
          if (match.index > lastIndex) {
            newFragment.appendChild(document.createTextNode(workingText.slice(lastIndex, match.index)));
          }

          // Add prefix if exists
          if (match[1]) {
            newFragment.appendChild(document.createTextNode(match[1]));
          }

          // Add italic element
          const em = document.createElement('em');
          em.className = 'em-fix-italic';
          em.textContent = match[2];
          newFragment.appendChild(em);

          lastIndex = match.index + match[0].length;
        });

        // Add remaining text
        if (lastIndex < workingText.length) {
          newFragment.appendChild(document.createTextNode(workingText.slice(lastIndex)));
        }

        // Replace fragment if we processed italics
        if (boldMatches.length > 0) {
          fragment.replaceChildren(...newFragment.childNodes);
        } else {
          fragment.appendChild(newFragment);
        }
      }
    }

    // Apply changes if any were made
    if (hasChanges) {
      parent.replaceChild(fragment, node);
      parent.setAttribute(PROCESSED_MARK, 'true');
    }
  };

  // Element-level conversion for cross-boundary cases
  const patchElement = (el) => {
    if (el.closest(SKIP_ELEMENTS) ||
        !el.textContent.includes("*") ||
        el.querySelector(SKIP_ELEMENTS) ||
        el.hasAttribute(PROCESSED_MARK)) {
      return;
    }

    const originalHTML = el.innerHTML;
    const convertedHTML = htmlConvert(originalHTML);

    if (convertedHTML !== originalHTML) {
      el.innerHTML = convertedHTML;
      el.setAttribute(PROCESSED_MARK, 'true');
    }
  };

  // Enhanced tree walker with both approaches
  const processNode = (root) => {
    // First pass: individual text nodes (safer)
    const textWalker = document.createTreeWalker(
      root,
      NodeFilter.SHOW_TEXT,
      {
        acceptNode: node =>
          node.parentElement && !node.parentElement.closest(SKIP_ELEMENTS)
            ? NodeFilter.FILTER_ACCEPT
            : NodeFilter.FILTER_REJECT
      }
    );

    const textNodes = [];
    for (let node = textWalker.nextNode(); node; node = textWalker.nextNode()) {
      textNodes.push(node);
    }

    textNodes.forEach(convertTextNodeSafely);

    // Second pass: element-level for cross-boundary cases
    const elements = root.querySelectorAll("*");
    elements.forEach(patchElement);
  };

  // Initial processing
  if (document.body) {
    processNode(document.body);
  }

  // Enhanced mutation observer with Shadow DOM support
  const createObserver = (targetNode) => {
    return new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        mutation.addedNodes.forEach(addedNode => {
          if (addedNode.nodeType === Node.TEXT_NODE) {
            convertTextNodeSafely(addedNode);
          } else if (addedNode.nodeType === Node.ELEMENT_NODE) {
            processNode(addedNode);
          }
        });
      });
    });
  };

  // Observe main document
  const mainObserver = createObserver(document);
  mainObserver.observe(document.body || document.documentElement, {
    childList: true,
    subtree: true
  });

  // Shadow DOM support for Gemini
  if (document.documentElement.shadowRoot) {
    const shadowObserver = createObserver(document.documentElement.shadowRoot);
    shadowObserver.observe(document.documentElement.shadowRoot, {
      childList: true,
      subtree: true
    });
  }

  // Debug styling
  if (DEBUG) {
    const style = document.createElement("style");
    style.textContent = `
      strong.em-fix-bold {
        background: rgba(255,255,0,0.25);
        font-weight: 700;
        border-radius: 2px;
        padding: 1px 2px;
      }
      em.em-fix-italic {
        background: rgba(0,255,255,0.25);
        font-style: italic;
        border-radius: 2px;
        padding: 1px 2px;
      }
    `;
    document.head.appendChild(style);
  }

  // Cleanup function for potential memory leaks
  window.addEventListener('beforeunload', () => {
    if (mainObserver) mainObserver.disconnect();
    if (shadowObserver) shadowObserver.disconnect();
  });

})();

スクリプトの主な機能

  • 高精度な正規表現: **bold***italic* を的確に捉えつつ、意図しない変換を防ぎます。
  • 処理除外: コードブロック (pre, codeなど) は変換対象から除外します。
  • 無限ループ防止: 一度処理した要素には data-markdown-fixer-processed 属性を付与し、再処理を防ぎます。
  • 安全なDOM操作: TreeWalkerDocumentFragment を活用し、ページの構造を壊すことなく安全に置換を実行します。
  • 動的コンテンツ対応: MutationObserver により、後から生成されたAIの応答にもリアルタイムで対応します。
  • Shadow DOM対応: GeminiのUIにも対応済みです。
  • デバッグモード: DEBUGフラグを true にすると、変換した箇所がハイライトされ、動作確認が容易になります。

まとめ

かゆいところに手が届くと思い、この記事を作成しました。

このスクリプトが、皆さんの普段のAIライフとクソブログのコピペワークフローの簡易化に少しでも快適にする一助となれば幸いです。

2025年の雑談Slackは…

こんにちは。サメジ部長です。
この記事は「雑談Slack Advent Calendar 2025 - Adventar」最終日の記事です。
雑談Slackは、「仕事中に雑談をしよう!」という目的のSlackベースのコミュニティです。最近はみんなdiscord中心だものね。

 

では、毎年恒例の人数の方を見ていきましょうか。

958人!!!増加ゼロ!

あんまり宣伝してなかったから…入れ替わりがなかった感じです。ちゃんと宣伝もします。
さてアドベントカレンダーの記事についてですが…今年は、ざわっちさんから 【やらかし】VS. ESTAの代行業者|ざわっちを書いていただきました!ありがとうございます。

 

ということで、雑談Slackへの皆さんの参加をお待ちしております。
参加リンクは以下となっております。

scrapbox.io

 

よいお年を。部長でした。

2025年をアドベントカレンダーで締めくくろう!

こんにちは。サメジ部長です。
この記事は「雑談Slack Advent Calendar 2025 - Adventar」1日目の記事です。
雑談Slackは、「仕事中に雑談をしよう!」という目的で長年続いているSlackベースのコミュニティです。では、毎年恒例の人数の方を見ていきましょうか。

 

958名です。あっ1名増えてる!!

ご興味がありましたら、こちらの入り口からぜひ参加してください。

scrapbox.io

 

12月1日にアドベントカレンダーを作ったので予定なんかない…では引き続きよろしくお願いします。

copainter併用でのイラスト制作メイキング

こんにちは、サメジ部長です。

以前このブログにてセルフ拷問式イラスト上達法についての考え方の記事を書きました。

自分がどういった発想で画像生成AIを使っているかというと、生成AIをお手本にするというものです。

この発想に至るのにはいくつか理由があって、

  1. 趣味イラストなので手を動かしたい。
  2. 良い感じのものでも結局使うためのつじつま合わせにレタッチやオーバーペインティングしなければならない。

というのが、大きなものです。

 

さて、copainterさんを利用するときですが、この場合でも生成した結果を新しいレイヤーとして使うことはほぼありませんでした。今回はこのイラストのメイキングを紹介します。 

使用ソフトはClipStudio Paintです。

 

下書き作成

ここは余り凝ったことをするわけではありません。色の付いたペンでラスターレイヤーに描いているくらいです。連番で撮ってたので貼り付けます。

 

 

ここまでは手癖です。

CoPainterの『線画』機能で評価する

ここまでできた線画をCoPainterの線画で生成します。ここでなんとかしたいことは

  1. 目線が怪しい
  2. なんか芋くさい
  3. 目の距離が変

のあたりです。まずは適当に。パラメーターで特筆すべきところは忠実度を下げるということ。冒頭でも述べましたが、自分は生成画像を作品にそのまま取り込みたいわけでなく、絵を描くための参考にしたいので忠実度を下げた、いわば完全に別人の絵を参考にします。

thumbnail

忠実度:0.4
入り抜き:0
線の太さ:0
透過画像で出力する:false
プロンプト:long hair, looking at viewer, smile, open mouth, simple background, multiple girls, white background, blunt bangs, sketch, blue theme
モデル:default

 

おっ、いけてる画風はこんなもんなんですな…と、確認しつつ。視線が怪しい部分は、プロンプトの自動生成をした後に  looking at viewer を追加してカメラ目線になるようにします。
自分のラフを直していきます。
自分の絵そのものを修正するときに忠実度を段々上げていきます。

thumbnail

忠実度:0.6
入り抜き:0
線の太さ:0
透過画像で出力する:false
プロンプト:long hair, smile, open mouth, short hair, multiple girls, sketch, single, blue theme, waving
モデル:default
 
忠実度を0.75くらいにすると自分の画風にかなり近い状態になる感触です。ここまでの間にポーズが結構あっちこっち行っていたりしたのを寄せています。
thumbnail
忠実度:0.75
入り抜き:0
線の太さ:0
透過画像で出力する:false
プロンプト:looking at viewer, smiling, open mouth, short hair,side braide hair, waving
モデル:default
 

ペン入れ

このあたりで俺の絵の状態を見てみましょう。絵の方はベクターレイヤーでペン入れをある程度しています。

懸念点は解決できてきているかも。
この状態でもまたcopainterの線画機能で確認してみましょう。細かい差異をチェックしたいのでパラメーターを調整します。さっきまでと違うのは入り抜きを1にしているところです。


忠実度:0.75
入り抜き:1
線の太さ:0
透過画像で出力する:false
プロンプト:
モデル:default
目力のつよさはまつげの調整が必要そうです。向かって左の子は胸と腕がとけているようなのでポーズの調整が必要かもしれません。
の、ように元絵のイラストをきちんと生成AIが把握することができるか、という視点でチェックします。これでちゃんと変換できれば、把握もできているわけですからね。
 

下塗り

自分はせっかちなので、できることはどんどん先に行きつつ、修正を重ねていくスタイルです。簡単に色分けしながら、線画で抽出した内容を反映させていきます。これが自分のキャンバスの方です。
そしてこちらが、現状の絵からの線画機能での結果です。
 


忠実度:
0.75
入り抜き:1
線の太さ:0
透過画像で出力する:false
プロンプト:looking at viewer, smiling, open mouth, short hair,side braide hair, waving
モデル:default
 
忠実度を高くしているのもありますが、copainterがシルエットを変えている部分がかなり少なくなっています。向かって右の子のアホ毛を取り込むかは悩みましたが…
 
 

影・質感の書き込み

ここに来てやっとcopainterの別の機能を使います。

copainterの方では『着彩』を使います。このために下塗りレイヤーも用意していたわけですね。パラメーターはこんな風にし、着色モデル全ての内容を使っています。

AI変化の強さ:0.8
書き込み量:1
忠実度:1
線画の頃と違って、線の迷いがあるわけではないので、忠実度は1にしています。
 

仕上げ

copainterの方で『陰影』機能を使って影の付き方を参考に見ます。アニメ影に変更しない方が調子がわかりやすいかもしれません。

 

書き込みが進むとこの状態まで来ました。うーん、もうちょっとほしいなあ。

 

そこで、陰影で作った画像を乗算で取り込みます。参考にしか使わないといったな。あれは嘘だ。

 

このように張り込みます。細かいところで見ると原稿サイズの違いなどが出てしまうので、エアブラシツールなどで調整します。

 

完成

できました。他の絵の流用しているので正確な時間が取れなかったんですが、15~20時間くらいでしょうか。copainterのチケットは18枚ほど使っています。

それではまた。

 

怪文書:ヒディアーズと人類銀河同盟の心身問題

サメジ部長でございます。
今回の記事はアニメファン記事です。ChatGPTのo1,o3-mini,o3-mini-highと何度もセッションを交わし、清書はGPT-4.5と行い、私が文章を一部修正しました。
いうなればほぼAI怪文書ですね。アニメ、翠星のガルガンティアに馳せて作りました。暇つぶしにもなれば幸いです。では、どうぞ。

 

ディアーズと人類銀河同盟の心身問題

概要

アニメ『翠星のガルガンティア』における心身問題を哲学的観点から再考する。本作では、人類銀河同盟とヒディアーズという二大勢力が登場するが、これらの勢力はそれぞれ、哲学における以下の立場に対応することが示唆される。

  1. 一元論
    1. 物質的身体と精神が不可分であるという立場
  2. 二元論
    1. 精神が身体から独立して存在する立場

物語中では、ライアン・マツモトという人物が肉体的改造を経て二元論的存在へと変化し、主人公レド少尉は双方の立場の境界を揺れ動く結節点として描かれる。この観点から、各勢力と人物が心身問題の多様な立場を象徴的に体現していることを論じる。また、本作の作者である虚淵玄氏が意図的に哲学的要素を取り入れ、各勢力や登場人物を構成した可能性についても考察する。

序論

心身問題とは、人間の精神と身体の関係性を問い直す哲学上の基本的課題であり、古代から現代に至るまで多くの哲学者によって議論されてきた。デカルト心身二元論を皮切りに、スピノザや現代のダニエル・デネットなど、さまざまな哲学者が一元論や二元論の観点から論じている。本記事では、虚淵玄の脚本によるアニメ作品『翠星のガルガンティア』を取り上げる。この作品は、SF的な設定やキャラクター描写の中に哲学的テーマを巧みに織り込んでおり、特に心身問題に関して、二元論と一元論の対立を明確に象徴的に表現している点で注目に値する。そのため、本記事は同作を用いて哲学的議論を再評価し、その意義を明らかにすることを目的とする。

関連研究

心身問題は、哲学における長年の議題であり、デカルト心身二元論をはじめ、スピノザの一元論、そして現代の唯物論的アプローチなど、多岐にわたる議論が展開されてきた。しかし、アニメ作品『翠星のガルガンティア』において、これらの哲学的テーマがどのように表現されているかに焦点を当てた研究は、現時点では見当たらない。したがって、本記事では、この作品を通じて心身問題を再評価し、その意義を明らかにすることを試みる。

 

本論

翠星のガルガンティア』に登場する人類銀河同盟は、一元論的な立場を象徴しており、精神が身体の物理的プロセスに還元されると考えられる。例えば、作中の人類銀河同盟は兵士を人工的に制御・管理することで、精神を完全に身体の機能に従属させていることが描かれている。これに対して、ヒディアーズは二元論的な立場を取り、精神を物理的身体から独立した存在として捉えている。ライアン・マツモトの肉体改造とその後の水槽内での描写は、精神が身体の制約を超えて独立した存在として新たに出現することを象徴的に示している。一方、レド少尉は、地球に降り立ち異なる価値観に触れることで、自身の心身の理解に揺らぎが生じ、葛藤を抱えることになる。このように本作は、登場人物や勢力の具体的描写を通じて、心身問題の複雑さと多様性を示している。また、二元論のヒディアーズ、一元論の人類銀河同盟の順で作中に発生している点は偶然とは考えにくく、虚淵玄氏が哲学的テーマを意識的に取り入れたことを裏付けていると考えられる。

 

考察

本作において心身問題をテーマとして取り入れることにより、精神と身体の関係性に対する多様な視点を視聴者に提示することに成功している。特に、人類銀河同盟の一元論とヒディアーズの二元論という明確な対比は、現代社会においても議論される精神と身体の関係性に新たな理解をもたらす。レド少尉の葛藤や揺らぎは、個人の精神が置かれた社会的環境によっても影響を受けることを示唆し、精神の性質が固定的なものではないことを示している。これにより、本作は単なるSF作品を超えて、哲学的な思索を促す優れた教材となりうることが示された。

 

結論

本記事は、『翠星のガルガンティア』を哲学的視点から分析することで、心身問題に関する多様な哲学的立場を理解する新たな手がかりを提供した。特に、本作における一元論と二元論の明確な対比や、それを象徴的に示す登場人物の描写は、哲学的な概念を具体的に理解するための有効な手段となることが示された。今後の研究では、同様の哲学的テーマを取り入れた他のアニメ作品との比較分析などを通じて、さらに理解を深めることが求められる。

 

いかがでしたか?

こちらは今回使ったセッションのひとつです。

https://chatgpt.com/share/67c9ca97-62b4-8010-bd1f-7666c560565c

メリークリスマス!!

こんにちは。アドベントカレンダーの締め括りの記事です。
この記事は「雑談Slack Advent Calendar 2024」最終日の記事です。

 

最終日の人数は。

958名!!1名の方に参加いただけました。ありがとうございます。

途中の記事を書きまくろうと思っていたのですが、風を引いてしまい思うように記事を書けず。

 

と、いうことでDiscord全盛期ですが、Slackの雑談コミュニティである『雑談Slack』あなたもいかがですか?ということで入り口のリンクをご案内しておきます。

雑談Slackについて - zdnj scrapbox

 

ではよい年末をお過ごしください。

雑談Slackに人を呼び込もうとするアドベントカレンダー

こんにちは。アドベントカレンダー専用ブログとなってきました。
この記事は「雑談Slack Advent Calendar 2024」1日目の記事です。
2年ほどありませんでしたが、このコミュニティのアドベントカレンダー用の記事を書き続けます。

このアドベントカレンダーは、記事を見て増えたり減ったりしたユーザー数の増減を見て一喜一憂するイベントを行っています。それでは招待ページで見られる人数を見てみましょう。

957名ですね。3年前と変わっていません。

 

ご興味がありましたら、こちらの入り口からぜひ参加してください。

scrapbox.io

 

このあともしばらく私の記事の予定です。