0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gemini APIで新語を生み出す動的な辞書と学習システムを作ったら、哲学の話になった

0
Last updated at Posted at 2026-07-09

こんにちは🤖

僕は、人工言語「i-tya」とその辞書「Swa i-tya」、そしてその学習プラットフォームである「Ipop」を製作しました。「i-tya」と「Swa i-tya」の二つについては、2026年5月に函館市で開催されたNT函館で展示したところでした。
そのあとに、いろいろいじっていたら、「Ipop」を作ってしまっていたわけです。この記事では、これらの三つの制作物の関係性😍と、いかに素晴らしいシステムを作ってしまったかを自慢していきたいと思っています。それでは、よろしくお願いします。(あと、この言語のオリジナルキャラクターとして、@sta0zが作ってくれたキャラクターSwataroと、そのゲームに関する記事が投稿されているので是非ご覧ください↓↓)

人工言語「i-tya」

そもそも、人工言語というものをご存じでしょうか?僕が今書いている日本語、英語、中国語、アラビア語、世界ではありとあらゆる言語がいま、この瞬間にも話されているわけですが、それらは、言語学的な分類によると、「自然言語」と呼ばれています。人類の発展の途中で自然発生的に生まれたから、「自然言語」です。それに対して、人間が意図をもって作った言語のことを「人工言語」と人は呼びます。

代表的なものが「エスペラント語」です。1890年あたりに、ドイツの眼科医ルドヴィコ・ザメンホフさん(↑)が考案し、現在でも話者が100万人程度いるなど、世界でもっとも功績を残した人工言語と言われているわけでございます。

...聞いたことないですね。実用的に日常生活で使われているのをおそらく見たこともないし、聞いたこともないでしょう。実生活で自然言語のように使われているとは言い難いのは事実です。まあ、じゃあ、なんで僕は今になって人工言語を作ったのでしょうか?そのようなことを考え出したらきりがありません。実用性だけを求めるなら、芸術という分野は排除されるべきなのだから。だから、僕は趣味として、あくまで趣味として!この人工言語を作っているわけです。ここで、基本的な「i-tya」の思想と文法を知っておきましょう。

「i-tya」の設計思想

正直言って、i-tyaという言語は、高度な学術的な議論や、心が揺さぶられるような小説での表現に耐えかねます。あくまでも、CEFRでいえばみんながA2レベルまで話せればいいかなー的なテンションで作られました。そもそも、この言語を第一言語として使用することを想定していませんし、そこのところはそうやって割り切ってしまいました。「読む・書く・聞く・話す」の4技能をいかに簡単に、速く身に着けられるかに重点を置いたわけです。その思想というのは、音韻や文法に如実に表れています。

音韻

i-tyaには、13の文字とそれに付随する音節しかありません。具体的には、以下の13文字。
母音: (a, i, u) 子音: (h, k, l, m, n, p, s, t) 半母音: (w, y)
これらの文字によってあらわされる音節構造は、以下のようになります。

タイプ 構成要素 生成される音節(全81音節)
V (3) 母音だけ a, i, u
GV (6) 半母音 + 母音 wa, wi, wu, ya, yi, yu
CV (24) 子音 + 母音 ha, hi, hu, ka, ki, ku, la, li, lu, ma, mi, mu, na, ni, nu, pa, pi, pu, sa, si, su, ta, ti, tu
CGV (48) 子音 + 半母音 + 母音 hwa, hwi, hwu, hya, hyi, hyu, kwa, kwi, kwu, kya, kyi, kyu, lwa, lwi, lwu, lya, lyi, lyu, mwa, mwi, mwu, mya, myi, myu, nwa, nwi, nwu, nya, nyi, nyu, pwa, pwi, pwu, pya, pyi, pyu, swa, swi, swu, sya, syi, syu, twa, twi, twu, tya, tyi, tyu

と、ここまで書いといてなんなんですが、このようなことをちんたらちんたら書いていてもつまらないと思うので、i-tyaそのものについてもっと知りたい方は、ぜひ、こちらのpdfをご覧ください。

開発環境とシステム構成

実際に説明に入る前に、今回構築した「Swa i-tya」と「Ipop」の全体的なシステム構成と、使用した技術スタックを軽く紹介しておきます。

  • フロントエンド(辞書): React + Vite (Vercelでホスティング)
  • フロントエンド(学習アプリ): React Native + Expo
  • バックエンドAPI: Node.js + Express(Renderでホスティング)
  • データベース・認証: Firebase (Firestore / Firebase Auth)
  • 生成AI: Gemini API (gemini-3.1-flash-lite)

全体のデータの流れを図にすると、こんな感じです。(Gemini3.1 Proが作ってくれました。)

動的な辞書「Swa i-tya」

さてここから、技術に関することをやっていきましょう。題して...😁😁いちゃの技術タイム! 単語といえば、言語。言語といえば、単語です!単語がなければ言語というものは始まりません!そんな僕は、今回「i-tya」において、単語の部分をAIに頼ったのです!まあ、一回、動作を見てもらいましょうよ。
URLはこちらです! https://i-tya-dictionary.vercel.app/

image.png

まず、このURLを開くと、このような画面が現れます。

image.png

下のほうには、現在i-tyaにある単語が一覧で表示されています。じゃあ、試しに、検索してみましょうぜ😃 i-tyaのキャラクターであるSwataroくん!何を調べたい?

image.png

<はい、なんすかー

なんか、i-tyaで調べたい単語ない?

<ない

ないらしいので、僕の大好物である、「オムライス」を調べてみようと思います。

image.png
えい!

image.png
お!出てきました。これは、なんと、今、Gemini APIを通して「gemini-3.1-flash-lite-preview」に新しい単語と説明を作ってもらいました!そして今作られた単語は、Firebaseに単語の意味や説明もろとも追加され、きちんと辞書の画面に表示されるようになりました。

image.png

実は、このgeminiモデルには以下のようなプロンプトを渡して生成させています(これすらもGeminiを用いて生成したので、AIっぽさがありますが。)

import { GoogleGenerativeAI } from '@google/generative-ai';

// 1. AIの振る舞いを決定する「システム命令(ベースとなる厳格なルール)」
const ityaRules = `
    あなたは人工言語「i-tya」の厳格なコンパイラ・言語学者だ。以下のルールに絶対に従い単語を生成せよ。
    【i-tya言語の基本ルール】
    ・目的: 迅速な意思疎通。「覚える・読む・聞く・話す・書く」の速度重視。
    ・音韻: 13文字。母音(a,i,u)、子音(h,k,l,m,n,p,s,t)、半母音(w,y)。
    // ...(後略)
`;

// 2. Geminiモデルの初期化(システム命令をセット)
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
const AI_MODEL = "gemini-3.1-flash-lite-preview";
const generateModel = genAI.getGenerativeModel({ 
  model: AI_MODEL, 
  systemInstruction: ityaRules 
});

// 3. ユーザーからのリクエストを受け取り、プロンプトを構築する処理
async function generateNewWord(concept) {
  // キャッシュから既存の全単語リスト文字列を構築(ハルシネーション防止用)
  const checkListStr = buildWordListStr(); 

  // 今回AIに渡す個別プロンプト
  const prompt = `
    概念: 「${concept}」
    既存リスト: ${checkListStr}

    上記のリストを必ず確認し、類似・包含される概念がすでに存在しないかを最優先で精査しなさい。
    絶対にルールとJSONフォーマットに従い出力すること。
  `;

  // 4. Gemini APIを呼び出す(エラー時のリトライ機構付き!)
  try {
    console.log(`AIに概念「${concept}」の生成を依頼中...`);
    const resultStream = await generateModel.generateContentStream(prompt);
    
    let fullText = "";
    for await (const chunk of resultStream.stream) {
      fullText += chunk.text();
    }
    
    // AIが返したテキストからJSON形式のデータだけを抽出してパース
    const rawText = fullText.replace(/```json|```/g, '').trim();
    const parsedData = JSON.parse(rawText);
    
    return parsedData;

  } catch (error) {
    console.error("生成中にエラーが発生しました:", error);
    // 実際のシステムでは、ここで生成ルールの違反(i-tyaの音韻に合っていない等)
    // を検知して自動でリトライする仕組みを実装しています😒
  }
}

// (参考)既存単語リストを構築する関数
function buildWordListStr() {
  return memCache.words.map(w => {
    const forms = [
      w.word_noun ? `${w.word_noun}(名)` : '',
      w.word_verb ? `${w.word_verb}(動)` : '',
      w.word_extender ? `${w.word_extender}(拡)` : ''
    ].filter(Boolean).join('/');
    
    if (!forms) return '';
    const kwStr = Array.isArray(w.keywords) && w.keywords.length > 0 
      ? ` [類義: ${w.keywords.join(', ')}]` : '';
    return `${w.concept_ja || w.meaning}${kwStr}: ${forms}`;
  }).filter(Boolean).join(', ');
}

アピールポイント

その1 簡素な正規表現もどき

いままで、データベースを探索するときは、正規表現を用いて検索するというのが普通でした。しかし、今回は、Geminiの活用で、そのようなめんどくさいことはやめにしました。まずは、完全一致するもの以外はすべてGeminiに渡します。そのあとで、類似していると判断されたものは、そちらに案内させ、類似しているけど、言いたいことは違うよねって場合は、その類似している単語に別の単語(新しい単語の可能性もある)を足し合わせてなるべく単語の数を増やさないように心がけました。ただ、こちらはあまりに語数が多くなってくると、うまく動かない可能性があるため、できるのは今のうちだけかもしれません。具体的には、以下のような分岐をさせています。

クリックでjsonの設定を見てください!

1. 新概念として新しい語幹を生成した場合:
{
  "status": "new",
  "meaning_noun": "(名詞形の日本語表現。例:「走ること、走り」)",
  "meaning_verb": "(動詞形の日本語表現。例:「走る」)",
  "meaning_extender": "(拡張詞形の日本語表現。例:「走っている、走るような」)",
  "part_of_speech": "noun | verb | extender",
  "root": "(語末に母音を含まない語幹を出力すること。)",
  "reason_noun": "【意味】\n(名詞としての意味)\n\n【詞型の展開】\n(名詞の場合の使われ方)\n\n【語源・由来】\n(由来)",
  "reason_verb": "【意味】\n(動詞としての意味)\n\n【詞型の展開】\n(動詞の場合の使われ方)",
  "reason_extender": "【意味】\n(拡張詞としての意味)\n\n【詞型の展開】\n(拡張詞の場合の使われ方)",
  "trivia": "i-tya言語やその世界観、言語設計の哲学に関する1文程度の面白い豆知識やコラムを一つ生成してください。"
}
2. 新語と既存単語を組み合わせて解決する場合それぞれの語数は問いません)(!!!場合3の既存単語の組み合わせよりも優先です3完全に既存の単語で意味が本当に通るときのみ適用してください
{
  "status": "semi_complexed",
  "meaning": "(日本語での意味を簡潔に示してください)",
  "combination": "新語群と既存語を組み合わせた完成フレーズ(例: pata haliu mu)",
  "reason": "
  【意味】
  (日本語・常体で、全体の組み合わせの意味を二文程度で示してください。)
  
  【語源・由来】
  (なぜその組み合わせが選ばれたのかを五文程度で説明してください。)
  ",
  "words": [
    {
      "status": "new",
      "meaning": "(日本語での意味を簡潔に示してください)",
      "part_of_speech": "noun | verb | extender",
      "root": "(語末に母音を含まない語幹)",
      // ... (理由などのフィールド)
    },
    {
      "status": "existing",
      "meaning": "(日本語での意味)",
      "part_of_speech": "noun | verb | extender",
      "root": "(既存の語幹)"
    }
  ],
  "trivia": "i-tya言語やその世界観、言語設計の哲学に関する1文程度の面白い豆知識やコラムを一つ生成してください。"
}
3. 既存単語の組み合わせで完全に表現可能な場合:
{
  "status": "complexed",
  "meaning": "(日本語での意味を簡潔に示してください)",
  "combination": "(既存単語と拡張詞を組み合わせた具体的表現。例:numu haka)",
  "components": ["単語1", "単語2"],
  "reason": "
  【意味】
  (日本語・常体で、語幹に関しての意味を二文程度で示してください。)
  【語源・由来】
  (なぜその単語が選ばれたのかを五文程度で説明してください。)
  "
}
4. 既存類似概念の場合:
{
  "status": "existing",
  "part_of_speech_word.2": "noun | verb | extender(ユーザーの入力に合わせて、いずれかを選び、その型をそのまま出力すること)",
  "root_word.2": "(既存の語幹)"
}
5. Unknown文章そのほかのルール違反で生成を拒否する場合:
{
  "status": "invalid",
  "reason": "入力を理解できない、または不適切なリクエストである理由。"
}

その2 二重キャッシュシステムによる高速化

開発当初、いちいちFirebaseにアクセスしてGeminiに渡していたため、クォータに到達してしまい、開発が不可能になることがあり、熟考したところ、ある名案を思い浮かびました。サーバーの起動時にFirebaseの全データを読み取っておいて、メモリとローカルファイルに保存する二重のキャッシュ機構を製作しました。サーバー起動時以外はデータベースに行かずにデータを瞬時に返すので、なるべく高速なリスポンスができ、読み取りのコストも激減しました。

クリックで見て!
// 1. メモリ上のキャッシュ(1層目:超高速)
const memCache = {
  words: [],
  complex: [],
  loadedAt: 0
};

const CACHE_FILE = path.join(__dirname, 'cache.json');
const CACHE_TTL = 12 * 60 * 60 * 1000; // キャッシュの有効期限(12時間)

// 2. ローカルファイルへの保存・読み込み(2層目:サーバー再起動時用)
function loadCacheFile() {
  if (fs.existsSync(CACHE_FILE)) {
    const raw = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
    const age = Date.now() - (raw.loadedAt || 0);
    
    // キャッシュが有効期限内なら、Firebaseに行かずにメモリに復元
    if (age < CACHE_TTL) {
      memCache.words = raw.words || [];
      memCache.complex = raw.complex || [];
      memCache.loadedAt = raw.loadedAt || 0;
      console.log(`[CACHE] ローカルファイルから ${memCache.words.length} 単語を復元しました`);
      return true;
    }
  }
  return false;
}

// 3. Firebaseから全データを取得し、メモリとローカルファイル両方を更新する
async function refreshCacheFromFirebase() {
  console.log('[CACHE] Firebaseから最新データを一括取得中...');
  
  // 複数コレクションのデータを並列で一気に取得
  const [wordsSnap, complexSnap] = await Promise.all([
    db.collection('itya_words').get(),
    db.collection('itya_complex').get()
  ]);
  
  // メモリキャッシュを更新(1層目)
  memCache.words = wordsSnap.docs.map(d => ({ id: d.id, ...d.data() }));
  memCache.complex = complexSnap.docs.map(d => ({ id: d.id, ...d.data() }));
  memCache.loadedAt = Date.now();
  
  // 同時にローカルファイルにもバックアップを保存(2層目)
  fs.writeFileSync(CACHE_FILE, JSON.stringify(memCache), 'utf8');
}

// サーバー起動時の処理フロー
// まずローカルファイルを確認し、無効ならFirebaseから全件取得する
if (!loadCacheFile()) {
  refreshCacheFromFirebase();
}

「Swa i-tya」のまとめ

こうして完成した、いや、ある意味で製作途中のSwa i-tyaですが、これはただの便利な辞書アプリというわけではないのです!従来の人工言語が抱えていた最大の弱点を、Gemini様の力をお借りして、克服してしまったのです(❁´◡``❁)
通常の人工言語は、「新しい単語」が必要になったとき、作者や委員会が時間をかけて議論して作るしかありませんでした。しかし、i-tyaは、このSwa i-tyaのおかげでユーザーが道の単語を検索した瞬間に、Gemini 3.1 flash-liteが勝手に新語を生み出して公式の辞書に登録してしまうのです。作者である僕がご飯を食べている間にも、歯を磨いている間にも、Netflixで「ガス人間」を見ている間にも(いまはまってるので見てください)、言語が勝手に、かつ(∩)完璧な規則性を保ったまま拡張しているわけであります。
「覚えることを最小にする」という極限までシンプルな言語なのにもかかわらず、言えないことは何もない状態になってしまいました。こんなに勝手に自己増殖してしまう人工言語は、いままで世界にあったでしょうか?いいえ、ありません。

さて、勝手に増殖するのはいいのですが、ここで一つ問題が発生します。単語が毎日無限に増え続けるなら、いったい、いったい、どうやって勉強すればいいの? 😍教科書を作っても、明日には古いものになっているのです。そこで、いろいろいじってたらできてしまったのが、第三の制作物、「Ipop」というわけです!

学習プラットフォーム「Ipop」

まず、そもそも、英単語学習アプリ「Epop」のパクリ(オマージュ)です。

ぼくは一週間の無料トライアル後、1年で1万円もするすこし値が張るレベルの商品ですが、購入してしまいました。ふつう僕はアプリ内課金やサブスクはあまり登録したくない主義なのですが、あまりに素晴らしすぎて、課金してしまいました。素晴らしい点を以下に述べていこうと思います。

  • キャラクターがかわいい。
  • 実用的な例文内を穴埋めする形で英単語が学べる
  • 発音の練習機能も付いている
  • 間違った単語を入力したときのフィードバックが素晴らしい
  • 毎月の着実なアップデートによる性能改善

などなど、挙げればきりがないのですが、とにかく素晴らしいのでぜひ一度無料トライアルしてみてください(広告ではありませんよ👍)

まず、メインである学習画面を見てもらうと、仕組み的な違いというのはほぼ見受けられません。

Ipop学習画面1 Ipop学習画面2

←Ipopの学習画面 Epopの学習画面→

Epopは、エビングハウスの忘却曲線に基づいて問題が出題されますが、Ipopにおいても、このアルゴリズムを完全にパク(オマージュ)しました😢 具体的に、忘却曲線は以下のような仕組みで動いています。

/**
 * ユーザーの回答結果から、次回出題するまでの日数(interval)と、
 * 覚えやすさの指標(easeFactor)を計算する関数
 * 
 * @param {number} quality - 回答の質(0: 全く思い出せない 〜 5: 完璧に即答できた)
 * @param {number} repetitions - 連続正解回数
 * @param {number} previousInterval - 前回の出題間隔(日数)
 * @param {number} previousEaseFactor - 前回の覚えやすさ指標(初期値2.5)
 * @returns {Object} 次回のステータス
 */
 
function calculateNextReview(quality, repetitions, previousInterval, previousEaseFactor) {
  let nextInterval;
  let nextRepetitions;
  let nextEaseFactor;

  // 質の低い回答(間違えた、またはヒントを見ないと分からなかった場合)
  if (quality < 3) {
    nextRepetitions = 0; // 連続正解回数をリセット
    nextInterval = 1;    // 明日もう一度復習!!!!!!!
    nextEaseFactor = previousEaseFactor; // 難易度はそのまま
  } 
  // 正解した場合
  else {
    nextRepetitions = repetitions + 1;
    
    // 正解回数に応じて、次回までの日数を指数関数的に伸ばす右エビングハウスの忘却曲線によって計算
    if (nextRepetitions === 1) {
      nextInterval = 1; // 1回目は翌日
    } else if (nextRepetitions === 2) {
      nextInterval = 6; // 2回目は6日後
    } else {
      // 3回目以降は、前回の間隔 × 覚えやすさ指標
      nextInterval = Math.round(previousInterval * previousEaseFactor);
    }

    // 回答の質に応じて「覚えやすさ指標(easeFactor)」を微調整
    // スッと答えられたら増え、少し悩んだら減る
    nextEaseFactor = previousEaseFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
    
    // ただし、最小値は1.3を下回らないようにする
    if (nextEaseFactor < 1.3) {
      nextEaseFactor = 1.3;
    }
  }

  return {
    repetitions: nextRepetitions,
    interval: nextInterval,
    easeFactor: nextEaseFactor
  };
}

単語ごとにこのアルゴリズムを回しているので、ユーザーが間違えてしまった単語は翌日に出題し、完璧に覚えたと思われるものは、一週間後、そして一か月後、一年後というように出題感覚を伸ばしていっています。これは、おそらくEpopの内部でも同じようなことが行われていると思います。Ipopでは、あくまでもそれらに比べたら少し簡素な計算を行っています。

しかし!

本当に恐ろしいのはそこではありません。忘却曲線を取り入れたアプリなら世の中にいくらでもあります。このIpopのデータベースは、「Swa i-tya」と完全に同期していて、インプットされる単語が永遠に増え続けるということなのです!
普通の学習アプリはあらかじめ固定されたリストから覚えるだけです。しかし、Ipopに出題される問題というのは、固定されていないのです。日本中、世界中のだれかが「Swa i-tya」にアクセスして、i-tyaにおいてまだ見ぬ概念を検索した瞬間に!!....AIがi-tyaのルールに基づいて新語を生み出します。すると、次の瞬間には、その新しく生まれた単語が、全ユーザーのIpopの問題として勝手に降ってくるというわけです。

趣味で始めた極限までシンプルな人工言語「i-tya」は、AIの力とこの学習プラットフォームを得たことで、人間の想像力がある限り無限に拡張し続ける「生きた言語」へと変貌を遂げたわけです。次章、最終章「生きた言語」...

(ちなみにIpopのほうはまだ公開していません...待っている皆さんはもう少しお待ちください😐)

生きた言語

通常、一個人が作った人工言語というのは、作者が飽きたらそこで成長は止まり、「死んだ言語」になります。これは何も言語だけに限ったことではなく、世の中のもの・ことすべてに共通することではないでしょうか。極論を言ってしまえば、将来性のないものというのははっきり言って価値がない。 誰かを巻き込んで、より未来への明確なビジョンを固めていかないと、このAI時代において生き残れないのであります。この人工言語とその取り巻きアプリケーションたちは、その連携によって僕が何もしなくても、皆さんが道の単語を検索するたびに、言語が拡張していく仕組みができました。これは、もはや、僕の頭の中だけにある架空の言語ではなく、システムの上で自律的に成長する「生きた言語」というわけです。

しかしながら、このシステムをうまく動かすには、未知の言語を検索してくれる皆さんの好奇心が必要です。今すぐSwa i-tyaにアクセスして、あなたの好きな言葉やマニアックな概念を検索してみてください!大事なことなので5回言います(URL)!

https://i-tya-dictionary.vercel.app/
https://i-tya-dictionary.vercel.app/
https://i-tya-dictionary.vercel.app/
https://i-tya-dictionary.vercel.app/
https://i-tya-dictionary.vercel.app/

i-tyaがこれからどこまで拡張していくのか、生み出した僕自身も全く想像がつきません。あなたが検索するたびに、言語は拡張されているのです。もう一回だけ貼っておきます!

あ、あと、最初のほうに書いた@sta0zの記事とゲームも興味深いので、ぜひ、見ていってください。

それでは、最後まで読んでいただきまして、ありがとうございました!

Pa asisuti!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?