入力後に言語を切り替える。「思考を止めないUX」を実現する状態管理の実装

Velocity Englishの近未来的なコックピット。中央コンソールから伸びる光の帯が、ホログラムのEN(英語)、ES(スペイン語)、FR(フランス語)へ繋がり、日本語の独り言が多言語へトランスミュートする様子を表現したサイバーパンクな図解。

前回の記事『AIで多言語をトランスミュート!学習効率を最大化する実装設計』にて、Velocity Englishをさらに進化させるためのプロダクト再定義を行いました。

今回からはその実装フェーズ第1弾として、「思考を止めない言語スイッチ」の実装――状態管理編を始めていきます。

従来の翻訳アプリは「入力前に言語を選ぶ」のが一般的。

しかし、それでは直感的な思考が途切れてしまいます。日本語を入力した後、ボタン一つで色を変えるように言語を切り替えられれば、ユーザーは「翻訳設定」にリソースを割くことなく、純粋なアウトプットに集中できるようになる。

これを実現する鍵が、Streamlitの st.session_state

これは、スクリプトが再実行されても「ブラウザのタブが閉じられるまでデータを保持し続けてくれる辞書型の箱」のようなものです。

Streamlitは操作のたびにコードが上から下まで再実行される性質を持っています。そのため、普通の変数では実行のたびにデータが初期化(破棄)されてしまい、「入力した文字が消える」「切り替えるたびにAPIを叩き直す」といった、UX的にもAPIコスト的にも致命的な問題が発生します。

今回は st.session_state を使い倒して、この挙動を支える「土台」を作っていきます。

2. 【設計思想】なぜ「後出しスイッチ」なのか?

ただ翻訳するだけなら、世の中には優れたツールが山ほどあります。それでも俺が「入力後に言語を切り替える」という挙動にこだわったのには、エンジニアとしての、そして一人の学習者としての明確な理由が3つある。

① 直感(思考)を邪魔しない

一般的な学習ツールは、まず「何語を話すか」をユーザーに選ばせる。だが、人間の思考はもっと自由で、とりとめがないものだ。一つに対して取り組む集中力が無いと言えばそれまでだが、 「今の独り言、スペイン語ならどう響く? いや、フランス語の方がしっくりくるかも」 そんな風に、思考の結果を見てから色を変えるように言語を切り替えたい。 最初に言語を固定してしまうという「手続き」を排除することで、脳のリソースをすべて「言葉を生み出すこと」に集中させる設計にした。

【設計のポイント1:直感(思考)を邪魔しない】

  • 現状: 多くのツールは、まず「これから何語で話すか」を先に決めさせる。
  • 思想: 「今の独り言、スペイン語ならどう響く?」という事後の好奇心を最大化したい。
  • 効果: 言語選択という「手続き」を思考の後に持ってくることで、学習の心理的ハードルを下げる。

② 「能動的選択」を記憶のトリガーにする

AIは入力された音声・テキストが何語かを自動判別する機能を実装することは出来る。しかし、便利すぎるものはかえって記憶に残りにくい。 あえて[ES]や[FR]といったボタンをユーザーにポチッと押させる。この「自分で言語を選んだ」という能動的なアクションを取った方がが、脳に対して「これは今から覚えるべき重要な情報だ」と認識させるスイッチになる。 「楽をさせる」のではなく「記憶にフックをかける」ために、あえての手動スイッチとする。

【設計のポイント2:能動的選択を記憶のトリガーにする】

  • 現状: AIによる自動翻訳は便利だが、受け身になりやすく記憶に残りにくい。
  • 思想: ユーザーが自分で「[ES]」ボタンをポチッと押す。この「自分で選んだ」という小さな能動的アクションを記憶のスイッチにする。
  • 効果: 「楽をさせる」のではなく「脳にフックをかける」ことで、定着率を向上させる。

③ 構造(Core + Meat)を多言語で使い回す

一度日本語を「構造」として解剖してしまえば、その骨組み(Core)は世界中の言語で共通して使える。 「後出し」で言語を切り替えることで、同じ骨組みを維持したまま、皮(言語)だけが次々と貼り替わっていく(トランスミュートする)体験ができる。これにより、「言語ごとのリズムや音の違い」だけを純粋に比較・抽出することが可能になるんだ。

【設計のポイント3:解剖結果(Core + Meat)の再利用】

  • 現状: 言語を変えるたびに、一から翻訳し直すのが一般的。
  • 思想: 一度日本語を「構造」として解剖したら、その骨組み(Core)は共通。皮(言語)だけを貼り替える(トランスミュートする)体験を作る。
  • 効果: 言語ごとの「リズムの違い」だけを純粋に比較・抽出することが可能になる。

3. 実装の核心:st.session_state による状態管理

StreamlitのRerun(再実行)モデルとst.session_state(セッション状態管理)の対比を示す図解。画面中央を上から下へ流れるタイムライン。左側の「通常の変数(st.session_state なし)」は、Rerunの波が来るたびにデータが砂のように崩れ洗い流される。右側の「Session State(st.session_state あり)」は、ネオンブルーに光る強固な「データの防波堤」によってデータが守られ、次の実行へ引き継がれる。保持されたデータは、多言語(EN, ES, FR)のホログラム出力へと繋がる。
Rerunの嵐からデータを守る「防波堤」 ―― Streamlitは操作のたびにコードを再実行(Rerun)します。通常の変数ではデータが初期化(洗い流)されてしまいますが、st.session_state という「データの防波堤」に保存することで、データを次の実行へ安全に引き継ぎ、爆速の多言語切り替えを可能にします。

Streamlitで「思考を止めないUX」を実現するための、具体的なコード戦略を紐解いていく。

【実装のポイント1:データの「定位置」を決める初期化】

app_state というネオンブルーに光るガラスの器の中に、さらにネオン枠で仕切られた results(解析データの棚)がある、ネストした辞書の図解。棚には「EN」「ES」「FR」と書かれたポケットが並んでおり、「EN」のポケットには既に英語の解析データが入っている(キャッシュ)。「ES」のポケットは空だが、AIが生成したデータが、光の帯となって吸い込まれていく様子(コストを守る「二重のガード」)。すべての言語パスは合流し、多言語の出力へと繋がっている。
コストを守る「二重のガード」と、賢く溜める「定位置」 ―― 一度解析した多言語のデータは、app_state[“results”] というネスト(入れ子)構造の中の、言語ごとのポケットに保存(キャッシュ)されます。これにより、二度手間なAPIリクエストを防ぎつつ、爆速の多言語切り替えを可能にします。

Streamlitは操作のたびにコードが最初から走り直す。そのため、まずは「箱(session_state)」の中に必要なスペースを確保する初期化処理が必要となる。

Python

import streamlit as st

# セッション状態の初期化
if "app_state" not in st.session_state:
    st.session_state.app_state = {
        "user_input": "",      # 入力された日本語
        "results": {},         # 各言語の解析データ(EN, ES, FRなど)
        "current_lang": "JP"   # 現在表示している言語モード
    }

if "key" not in st.session_state: というガードを置くことで、再実行されても既存のデータがリセットされず、未定義エラーも防ぐことができる。

実装順序としては、まず『器(Session State)』を完璧に定義し、その後に『ロジック(引数の追加など)』を弄るのが鉄則。ここを同時に変えようとすると、バックエンドが新しい引数を受け取れずに一時的なエラー(TypeError)を吐き続け、開発のテンポを損なうことになるからだ。

【実装のポイント2:状態を切り替えるコールバック関数】

ボタンを押した瞬間に「表示言語」を切り替えるには、ボタンの on_click プロパティを活用する。

Python

def switch_lang(lang_code):
    # 表示する言語フラグを更新するだけの、極めて軽量な処理
    st.session_state.app_state["current_lang"] = lang_code

# UI部分:各言語のボタン
cols = st.columns(4)
languages = ["JP", "EN", "ES", "FR"]

for i, lang in enumerate(languages):
    cols[i].button(lang, on_click=switch_lang, args=(lang,))

ここで重要なのは、**「ボタンを押してもAPIを叩かない」**という設計だ。current_lang というフラグを書き換えるだけで、表示ロジック(後述)にバトンを渡す。

【実装のポイント3:キャッシュ(Results)の再利用】

一度AIが生成したデータは results 辞書に保存しておく。これにより、言語を往復しても再生成(APIコスト発生)が起きない「爆速の切り替え」が可能になる。

Python

target = st.session_state.app_state["current_lang"]

if target == "JP":
    st.write(st.session_state.app_state["user_input"])
elif target in st.session_state.app_state["results"]:
    # すでに解析済みなら、保存されたデータ(キャッシュ)を表示
    display_result(st.session_state.app_state["results"][target])
else:
    # データがなければここで初めてAIを呼び出す(次回へ続く!)
    pass

この「状態の分離」こそが、この記事で言いたかった「色を変えるように言語を切り替える」体験の技術となる。

4. 泥臭い検討:キャッシュ vs セッション

「爆速のUX」を取るか「APIコストの削減」を取るか。どちらも捨てがたい選択肢。

これは二律違反するテーマで、どちらか一方しか選び取ることは出来ない。

だが、どちらも工夫次第で選び取ることが出来る。そのために必要なのが、データの扱い。つまりはデータの保存場所の検討だ。

データをキャッシュとして保存するか、データをブラウザのセッションで保有するか。

今回は後者を選択する。

【検討のポイント1:なぜ st.cache_data ではなく st.session_state なのか?】

Streamlitには st.cache_data という強力なキャッシュ機能がある。しかし、今回はあえて st.session_state をメインに据えた。

  • st.cache_data: サーバー側に保存され、全ユーザーで共有される。
  • st.session_state: ブラウザのセッションごとに独立している。

ユーザーが入力する日本語は千差万別だ。誰かが入力した「こんにちは」の解析結果をサーバー全体でキャッシュしても、他のユーザーに再利用される確率は極めて低い。それどころか、プライバシーの観点からも「ユーザー個別の箱」であるセッション管理の方が安全だと判断した。

【検討のポイント2:コストを守る「二重のガード」】

API(Gemini)を叩くのは、お金がかかる「贅沢な処理」だ。一般的にはChatGPTやGeminiは無料で使えているからコストを払う必要なんてないだろ?と思うかもしれないが、本来はコストがかかっている。

そこで、無駄なリクエストを防ぐために以下のロジックを組む

  1. 解析済みチェック: if target in st.session_state.results: で、一度生成した言語は二度とAIに投げない。
  2. 入力変更の検知: もし元の日本語が書き換えられたら、古いキャッシュ(results)をすべてクリアする。

【検討のポイント3:ライフサイクルの設計】

このデータはいつまで保持すべきか? 「タブを閉じたら消えていい。でも、タブが開いている間は、英語からスペイン語に切り替え、また英語に戻った時に一瞬で表示されなければならない。」 この「短期的だが絶対的な保持」という要件に、st.session_state のライフサイクルが完璧に合致している。

技術的には全言語を同時に生成する『並列処理』も可能ですが、今回はあえて見送りました。YAGNI(You Ain’t Gonna Need It)の原則に基づき、ユーザーが本当に必要とした時だけリソース(APIコスト)を使う設計の方が、プロダクトとして誠実で堅牢だからです。

【Tips:関数の「後方互換性」を保つ設計】 今回、generate_sentence 関数を多言語対応に拡張しましたが、ここで一つ工夫したのがデフォルト引数の活用です。

Python

def generate_sentence(japanese_input: str, lang="EN"):
    # langが指定されなければ、自動的に英語(EN)として処理する

プログラムを大規模に改修する際、いきなり関数の形を変えてしまうと、修正が追いついていない他の箇所でエラーが多発します。あえて lang="EN" とデフォルト値を設定しておくことで、古い呼び出し箇所を壊すことなく、安全かつ段階的に機能を拡張できる。こうした「壊さない工夫」が、スピード感のある開発には不可欠です。

. まとめと次回の予告

第1回では、st.session_state を活用した状態管理の実装について解説しました。 「入力した後に言語を選ぶ」という、一見シンプルですが、Streamlitの再実行モデル(Rerun)を逆手に取ったこの設計こそが、Velocity Englishの「思考を止めない体験」の正体です。

  • 初期化の徹底: データの「定位置」を確保し、エラーを防ぐ。
  • コールバックの活用: UI操作と重い処理を切り離し、爆速のレスポンスを実現する。
  • セッションの最適化: ユーザー個別の「箱」でデータを守り、APIコストとプライバシーを両立させる。

「どう動かすか」の前に「どうあるべきか」を設計する。この泥臭い土台作りが、プロダクトの堅牢さを決めます。

【次回の予告:多言語をハックする「知能」の設計】

土台は整いました。しかし、この「箱」の中に詰め込む多言語の解析データは、一体どうやって生成しているのか?

英語、スペイン語、フランス語……それぞれの言語が持つ固有のリズムや構造を、Geminiに対してどのように命令し、抽出させているのか。

次回、「プロンプト設計編:config.yaml で実現する多言語プロンプト・インジェクション」。 ハードコーディングを一切排除し、YAMLファイル一つでアプリの「知能」をスケーラブルに拡張する、エンジニア必見のプロンプト設計思想を公開します。

お楽しみに!

  • 土台(状態管理)が完成したことで、次は「どうやって多言語を解析するか」という中身の話へ。
  • 次回予告: config.yaml を使った多言語プロンプト設計。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

コメントは日本語で入力してください。(スパム対策)