はじめに:クラウドAI全盛期における「パラノイア(恐怖)」
ChatGPTやClaudeなど、強力なクラウドAIの登場により、私たちのテキスト処理の生産性は劇的に向上しました。しかし、その影で強いパラノイア(恐怖)を抱えている人々がいます。
「未発表の小説アイデアや、クライアントとのNDA(秘密保持契約)に縛られた極秘原稿を、外部サーバーにアップロードして本当に大丈夫なのか?」
一度クラウドに送信されたデータが、将来の学習モデルにどう使われるか、完全にコントロールすることは不可能です。「削除」ボタンを押しても、裏でログが残っているのではないかという疑心暗鬼。機密を扱う小説家やプロライターにとって、これは死活問題です。
そこで我々のチームは、「通信0 bytes(完全オフライン)」 で動作し、絶対に情報漏洩しない文章解析ツール『Zero-Leak Manuscript Analyzer』の開発を決意しました。
技術選定:Python + marimo + Pyodide (WASM)
要件は「ブラウザ内で完全に処理が完結し、外部サーバーと一切通信しないこと」。
これを実現するために選んだ技術スタックが、Python + marimo + Pyodide (WASM) です。
marimo は次世代のPythonノートブック環境であり、Pyodideを通じてブラウザ上のWebAssembly(WASM)環境で直接Pythonコードを実行(エクスポート)できます。
このアーキテクチャの最大のメリットは、インフラ費用(サーバー維持費)が一生「0円」 であること。そして何より、ユーザーに対して 「物理的にデータが外に出ない」という絶対的なセキュリティの証明(USP) を提示できることです。
我々はPythonの標準ライブラリ(reモジュール等)のみを駆使し、基本統計、NGワード検知、表記ゆれチェック、SEO構造分析を行う堅牢なロジックを組み上げました。
しかし、真の地獄は「フロントエンド(UI)のカスタマイズ」に潜んでいました。
直面した最大の壁:ダークテーマと「Shadow DOM」の罠
このアプリは、サイバーセキュリティツールのような「重厚な安心感」を演出するため、深いネイビーを基調としたダークモードUIを採用しました。
問題が起きたのは、入力欄(mo.ui.text や mo.ui.text_area)のスタイリングです。
背景が暗いため、文字色(フォアグラウンド)を白系(#e2e8f0)に設定していました。しかし、入力欄自体は白背景にしたかったため、そこに入力されるテキストも白くなってしまい「視認不可」になる事態が発生したのです。
そこで、入力テキストを黒色にするため、CSSで以下のように指定しました。
marimo-text { color: #111827 !important; }
するとどうでしょう。入力欄の文字は黒くなりましたが、入力欄の外側にある「ラベル(🚫 NGワードなど)」の文字まで真っ黒に潰れ、ダークな背景に同化して消えてしまったのです。
「ならばラベルだけ白にすればいい」と考え、次のように書きました。
marimo-text label { color: #e2e8f0 !important; }
……効きません。
なぜか?
marimoのUIコンポーネントは、Shadow DOM によってカプセル化されているからです。
グローバルな <style> から指定したCSSは、Shadow DOMの境界を越えられません。かといって、親要素である marimo-text に color を指定すると、CSSの継承によってShadow DOM内部にある「ラベル」と「入力欄」の両方に同じ色が波及してしまいます。
| 試みたCSSハック | 入力テキスト | ラベル | 結果 |
|---|---|---|---|
marimo-text { color: #111827 } |
⬛ 黒(読める) | ⬛ 黒(見えない) | ❌ 絶望 |
marimo-text { color: #e2e8f0 } |
⬜ 白(見えない) | ⬜ 白(読める) | ❌ 絶望 |
marimo-text label { color: #e2e8f0 } |
— | ❌ 当たらない | ❌ 絶望 |
ラベルと入力欄が同じShadow DOMの檻の中にいる限り、外部のCSSから両者を個別に制御することは不可能だったのです。
さらに残酷なことに、複数行入力であるmarimo-text-areaの内部は単なる<textarea>ではなく、リッチエディタである CodeMirror で構成されています。
入力のたびに数十個の<span>タグが動的に生成されるため、Shadow DOMの外側から文字色を完全制圧することは、まさに「悪夢」のような作業でした。
ラベルと入力欄が同じShadow DOMの檻の中にいる限り、外部のCSSから両者を個別に制御することは不可能だったのです。
ブレイクスルー:ゴルディアスの結び目を断つパラダイムシフト
数時間のCSSとの泥沼の格闘の末、我々は一つの決断を下しました。
「CSSで戦うことを諦め、DOM構造そのものを変える」
そもそも、なぜラベルがShadow DOMの中に囚われているのか?
それは、コンポーネントの引数として label="..." を渡しているからです。
# Before(問題のある構造:ラベルがカプセル化され、CSSが両方に波及する)
ng_words_input = mo.ui.text(
label="🚫 NGワード",
placeholder="例: 機密,未発表",
full_width=True
)
それならば、引数でラベルを渡すのをやめ、Shadow DOMの外側(Light DOM)に独立したHTML要素としてラベルを配置すればいいのです。
# After(解決した構造:label= パラメータを完全削除し、外側に分離)
# 【セルA: UIコンポーネントの定義】
ng_words_input = mo.ui.text(
placeholder="例: 機密,未発表",
full_width=True
)
# 【セルB: レイアウトの構築】
# ※定義と配置を別セルに分けるのが、marimoのReactive Loopを防ぐ鉄則です
mo.vstack([
mo.Html("<span class='ma-input-label'>🚫 NGワード(カンマ区切り)</span>"),
ng_words_input,
], gap=0)
このパラダイムシフトにより、CSSの制御は劇的にシンプルになりました。
/* Shadow DOM 外の純 HTML なので、確実に白を適用できる ✅ */
.ma-input-label {
color: #cbd5e1 !important;
}
/* Shadow DOM 内は「入力欄のみ」になったため、黒を指定しても副作用ゼロ ✅ */
marimo-text, marimo-text-area {
color: #111827 !important;
}
DOMの構造を図解するとこうなります。
【従来:CSSが両方に伝播】
Shadow DOM ホスト要素
└── Shadow DOM 内部
├── <label>ラベルテキスト</label> ← 黒になる ❌
└── <input>入力欄 ← 黒になる ✅
【解決後:完全に分離】
Light DOM(我々が完全制御)
└── <span class="ma-input-label">ラベル</span> ← 白になる ✅
Shadow DOM ホスト要素
└── Shadow DOM 内部
└── <入力欄のみ> ← 黒になる ✅
複雑に絡み合ったCSSのハック(ゴルディアスの結び目)を解こうとするのではなく、DOM構造を変更するという剣で一刀両断にした瞬間でした。
まとめ:技術の泥沼にはまったら「視座」を上げる
フレームワーク(今回であればmarimo)が提供する便利な機能(label= 引数)に依存しすぎると、かえって制約に縛られ、身動きが取れなくなることがあります。
「CSSが効かない」と目の前のコードとにらめっこするのではなく、「なぜ効かないのか(Shadow DOMの非対称性)」という一段高いアーキテクチャの視座に立つことで、驚くほどシンプルな解決策(ラベルを外に出す)が見つかります。
こうして完成した『Zero-Leak Manuscript Analyzer』は、一切の通信を行わない絶対的な安全性と、プロの執筆環境にふさわしい美しいダークUIを両立させることができました。
WASMやPyodideを使った完全フロントエンドアプリの開発は、制約が多い分、パズルを解くような極めてエキサイティングな体験です。この記事が、同じようにShadow DOMの壁にぶつかったエンジニアの突破口になれば幸いです。
最後までお読みいただき、ありがとうございました!
おまけ:成果物(デモ)
開発した 文章解析・推敲ツール がこちらです。
リンク先のサムネイル等で、UIを確認してみてください。
▼ Zero-Leak Manuscript Analyzer(絶対秘密保持・ローカル原稿アナライザー)