英語学習アプリ
TOEIC対策向けの英語学習アプリです。学ぶ単位を、英文・意味句・イントネーション・ユニット(IU)・単語の4つにそろえ、表のように選んで、1枚のカードで学びます。以前に作った試作のアプリ(公開は終了しました)を元に、設計から作り直しました。英文は端末に保存し、クラウド同期を使うときは端末の中で暗号化してから送ります。
このページは、その仕組みと、作るうえで判断したことの解説です。
試作から、何を変えたか
試作は、機能ごとに画面が並んでいました。新しいアプリは、使う順に「登録 → 学習対象選択 → 学習」の3段にしました。構成は、スプレッドシートに、機能を行として書き出して考えました。
| 試作 | 新しいアプリ | |
|---|---|---|
| 学ぶ単位 | 英文と単語 | 英文・意味句・IU・単語 |
| 画面 | 機能ごと(モード) | 登録 → 選択 → 学習 |
| 選び方 | 一覧から1文ずつ | 表で、チェック・ランダム・グループ・絞り込み |
| 保存先 | localStorage(約5MB) | IndexedDB |
| 発音記号 | 単語だけ | 単語・意味句・IU |
4つの単位
| 単位 | 中身 | 区切りの基準 |
|---|---|---|
| 英文 | 1文 | — |
| 意味句 | 英文を、意味のまとまりで区切ったもの(和訳つき) | 意味 |
| IU | 英文を、息継ぎ・音の区切りで区切ったもの | 音 |
| 単語 | 単語・熟語(意味・品詞・頻出度・例文つき) | — |
意味句とIUは、どちらも英文を前から区切りますが、区切る場所は同じとは限りません。そこで、区切りは、それぞれ別に持ちます。ただし、どちらも、つなぐと原文と一致するものだけを受け付けます。
function iu(list, text) {
// …各区切りの t を確かめる
return norm(out.map(x => x.t).join(" ")) === norm(text) ? out : null;
}
英文の表に、意味句(slash)とIU(iu)を持たせ、「英文から取り出す」で、意味句・IU・単語の表を作ります。難易度とグループは、その英文のものを引き継ぎます。
登録
登録の経路は、4つあります。
- サンプル:オリジナルの英文90文を、解析つきで入れます。APIは要りません。
- 貼り付け・ファイル:
.csv・.txt・.json。同じ英文は、足りない項目だけを足します(解析・状態・記録は上書きしません)。 - AI(Gemini):和訳・意味句・区切りの形・品詞・IU・単語・文法・TOEICの観点・4択を、まとめて付けます。方法は2つ。Gemini APIは、1文ずつ自動で呼びます(止められる。エラーで中断)。コピペモードは、APIキーが要りません。プロンプトを Gemini に貼り、返事を貼り戻します(5文ずつ)。
- 受信箱:Claude Code などが、英文を預けます(後述)。
AIの返事は、そのままでは入れません。区切りをつなぐと原文と一致するか、品詞コードが正しいか、4択の形が正しいか、を検査し、通らない部分だけ見送って、ほかは入れます。入れた英文は「未確認」になります。APIキーは、この端末のブラウザの中だけに保存し、バックアップや同期には入れません。
samples のIUは、位置の番号だけを答えさせて作った
samples の90文には、IUも先に入れました。ここでも、AIに文字を書き写させると、ずれやすいので、語を並べた表を渡し、「何番目から何番目まで」だけを答えさせました。機械で確かめたのは、すき間も重なりもなく全部の語を覆っていること、1文あたり1〜6個であること、1つが10語以内であること、です。90文とも通りました。249個のIUの語数は、4〜7語が中心です。
発音記号(IPA)
発音記号は、AIに書かせず、発音辞書から作ります。AIに任せると、もっともらしい誤りが混じるためです。辞書は、CMU Pronouncing Dictionary(Carnegie Mellon University が作った、アメリカ英語の発音辞書)を使います。
どこに付くか
- samples:単語・意味句・IUのすべてに、辞書から作ったものを、あらかじめ入れてあります。
- AIで解析した英文:意味句・IU・新しく入る単語に、辞書から付けます。AIが付けた発音記号は、辞書で作れる語については、辞書のものに置き換えます。辞書に無い語(
résuméなど)は、AIのものを残します。 - 貼り付けた単語・すでに入っているもの:単語を貼り付けると、自動で付きます。「登録」の「発音記号を辞書から付ける」で、発音記号が無い単語・意味句・IUに、まとめて付けられます(すでにあるものは、そのままです)。
辞書の作り方
辞書の音は、ARPABETという記号(AH0・K など)で書かれています。tools/build-pron.mjs が、これをIPAに直して、アプリが読む public/dict/cmudict-ipa.txt(1行に「単語|IPA」)を作ります。
- 強勢の印を、音節の頭に置く:辞書は、強勢を母音に付けます。IPAでは、強勢の印(ˈ ˌ)は音節の頭に置くので、母音の前の子音のまとまりのうち、音節の頭になれる並び(
pl・strなど)を選んで、その前に置きます(kəmˈplit、ˈstɹætədʒi)。 - 載せるのは、英字・アポストロフィ・ハイフンだけの語(約12.6万語)です。
- ファイルは約2.7MBですが、配信のときに圧縮されるので、通信量は約0.8MBです。必要になったときに、1回だけ読み込みます。発音記号がそろっているサンプルだけを使うときは、読み込みません。通信できないときは、何もせず、あとで付けられます。
区切りの発音記号
意味句・IUの発音記号は、語ごとの辞書の発音をつないだものです。
of・to・theなどの機能語は、弱い発音(əv・tə・ðə)にします。'sは、前の語の音にzを付けます。ハイフンでつないだ語は、辞書にあればその発音を、なければ前後に分けてつなぎます。- 1語でも辞書に無ければ、その区切りには付けません(途中が抜けた発音記号を出さないためです)。数字・固有名詞などを含む区切りは、付かないことがあります。
- 文全体での、弱い発音や音のつながりは出ません。
確かめたこと
samples の意味句・IU・単語、976件について、アプリが辞書から作る発音記号を、samples にあらかじめ入れてあるものと比べました。辞書で作れた963件はすべて一致し(相違0件)、残りの13件は、辞書に無い語(résumé・intranet・数字など)を含む区切りでした。
作るなかで直したこと
- 辞書の注釈:辞書の一部の値に、
# …という注釈が付いていて、変換でエラーになりました。注釈を取り除いてから変換するようにしました。 - ハイフンの語:最初は、ハイフンを含む語を辞書に載せていませんでした。そのため、
full-timeを、fullとtimeに分けて、それぞれに強い強勢(ˈ)が付きました。辞書には、full-timeの発音(強い強勢と、弱い強勢が1つずつ)があるので、ハイフンの語も載せるようにして、一致しました。
出典:CMU Pronouncing Dictionary。使用条件は、元の辞書の配布元の表示に従います(public/dict/NOTICE.txt)。
学習対象選択
どの単位でも、同じ3つの選び方です。
- チェックボックス:表で選ぶ。並べ替え・絞り込み・全選択ができる
- ランダム:絞り込んだ範囲から、N件
- グループ:グループで絞る
絞り込みは、グループ・状態・難易度に加えて、意味句とIUでは、語数(1〜2/3〜5/6以上)と文中の位置(文頭・文中・文末)で行えます。選んだ結果が「学習セット」になり、学習画面は、その並びを順にたどります。
学習
- 英文:ボタンは、「聞く」「調節」「表示」の3行です。全文は、いつも一番上に出し、その下に、「表示」で選んだ区切り(音区切り・音のつながり・意味区切り・文法区切り)を、同時に重ねられます。「表示」は、品詞・和訳・IPA・文法・単語・TOEIC解説・4択・間違いの記録も含めて、すべて独立した入り切りです(次の「英文の画面のボタン」を参照)。聞くは、全文・区切って・シャドーイング・音のつながり・聞き取り(次の節以降)。
- 単語・意味句・IU:1枚のカードで学びます。意味(意味句は和訳)は隠してあり、見ると復習が1回増えます。IUは、声に出して練習したら「練習した」で、復習が1回増えます。意味句とIUは、文の中での位置と、文の和訳も見られます。
- ショートカット:← → で前後、スペースで読み上げ、数字やアルファベットで表示・補助の切り替え。
英文の画面のボタン:聞く・調節・表示
英文の画面のボタンは、何をするボタンかで、3つの行に分けています。
| 行 | ボタン |
|---|---|
| 聞く | ▶全文を聞く/▶区切って聞く/シャドーイング/音のつながり/聞き取り/止める |
| 調節 | 速さ/間/繰り返し(間と繰り返しは、シャドーイング用) |
| 表示 | 和訳/IPA/音区切り/音のつながり/意味区切り/文法区切り/品詞/文法/単語・熟語/TOEIC解説/4択問題/間違いの記録 |
画面の上に固定されるのは、「聞く」の行だけです。3行すべてを固定すると、英文を表示する場所が狭くなるためです(スマホで、特に効きます)。「調節」と「表示」は、ふつうにスクロールして流れます。
表示は、すべて独立した入り切り
最初は、区切りの見方を、1つだけ選ぶ形でした(選ぶと、ほかが消える)。しかし、「意味で区切った訳を見ながら、文法の区切りも見たい」ができません。そこで、「表示」の12個を、すべて独立した入り切りにしました。
- 全文は、いつも一番上に出ます(ボタンはありません)。切り替えると全文が消える形だと、区切りと元の1文を、見比べにくいためです。
- 区切りの見方(音区切り・音のつながり・意味区切り・文法区切り)は、同時に出せます。出した順ではなく、決まった並び(音区切り → 音のつながり → 意味区切り → 文法区切り)で、全文の下に、縦に並びます。
- 出した区切りごとに、何を基準に区切っているかの1行の説明が付きます(例:「【意味区切り】意味のまとまりで区切り、各区切りに日本語訳を付けています…」)。見方が1つだったときは、説明も1行でしたが、同時に出すので、ブロックごとの見出しにしました。
- 「品詞」は、区切りではなく語ごとの色なので、見方とは別の入り切りで、全文にも、出している区切りのすべてにも重なります(後述)。
- 初期の状態は、「和訳」だけがオンです。設定は覚えていて、次に開いたときも残ります。
名前に、区切りの基準を入れた
最初は、見方の名前が「スラッシュ」「区切りの形」「IU」で、何を基準に区切っているかが、名前から分かりませんでした。そこで、基準が分かる名前に変えました。
| 見方 | 旧名 | 区切りの基準 | 付く情報 |
|---|---|---|---|
| 意味区切り | スラッシュ | 意味の切れ目 | 各区切りの日本語訳。前から順に意味を取る練習(スラッシュリーディング) |
| 文法区切り | 区切りの形 | 文法の単位 | 各区切りの形の名前(主語・関係詞節・前置詞句など) |
| 音区切り | IU | 音の切れ目(一息で読む単位) | 発音記号。シャドーイングは、この区切りで進む |
| 音のつながり | つながり | くっついて聞こえる所 | 発音記号(つながる所は ‿) |
同じ英文で比べると、基準の違いが出ます(アプリの実際のデータです)。
Ms. Tanaka, who joined the marketing team last spring, has been promoted to senior manager.
- 意味区切り:
Ms. Tanaka,(田中さんは)/who joined the marketing team last spring,(昨年の春にマーケティングチームに加わった)/has been promoted(昇進した)/to senior manager.(シニアマネージャーに) - 文法区切り:同じ4つに、
主語/関係詞節/動詞句/前置詞句 - 音区切り:
Ms. Tanaka,/who joined … spring,/has been promoted to senior manager.(一息で読めるように、後ろの2つが1つになる)
区切りが似ていても、持っている情報が違います(訳/形の名前/発音)。画面の見た目は同じでも、「何のための区切りか」が違うので、ブロックごとの説明で、その違いを示しています。内部のデータの名前(slash・phrases・iu)と、受信箱・バックアップの形式は、変えていません。変えたのは、画面の名前と説明だけです。
const VIEWS = [ // 表示するときの並び順(ボタンの並びと同じ)
{ k: "iu", note: "【音区切り】息継ぎの切れ目(一息で読む単位。…)で区切っています。…" },
{ k: "links", note: "【音のつながり】くっついて聞こえやすい語のまとまりを示しています。…" },
{ k: "slash", note: "【意味区切り】意味のまとまりで区切り、各区切りに日本語訳を付けています。…" },
{ k: "phrases", note: "【文法区切り】文法の単位(主語+動詞・前置詞句・関係詞節など)で区切り、…" }
];
「区切って聞く」は、出している区切りで読む
「▶区切って聞く」は、いま表示している区切りで、1区切りずつ読みます。複数出しているときは、「音区切り → 意味区切り → 文法区切り」の順で、最初に出ているものを使います。何も出していないときは、「文法区切り → 意味区切り → 音区切り」の順で、あるものを使い、その区切りを、自動で表示に出します(読んでいる区切りが、色付きで見えるようにするためです)。
- シャドーイングは、音区切りがあれば音区切り、なければ文法区切り、意味区切りの順で進み、使う区切りを、自動で表示に出します。
- 「音のつながり」を聞くは、つながりのブロックを、自動で表示に出します。
- 読んでいる区切りは、その区切りのブロックだけで、色が付きます。
品詞は、見方ではなく入り切り
品詞の色を、「意味区切り」「文法区切り」と同じ、選ぶ列に並べると、4択のように見えて、「品詞の色を見ながら、文法の区切りも見たい」ができません。品詞は、区切りではなく、語ごとの色だからです。そこで、入り切りにして、どの表示にも(上の全文にも)重ねます。
- 文字の色だけで表します。区切りの下線(文法の種類の色など)と、重ならないようにするためです。
- 区切りをつなぐと、原文に戻るので、語の並びの順に、品詞を当てます(全文用と、区切りのブロックごとに、別々に数えます)。ずれていたら、その語は、色を付けません。
- 品詞の情報が無い英文(貼り付けただけの英文など)では、色は付かず、「品詞の情報がないので、品詞の色は付きません」と出します。
ショートカットと、古い設定の引き継ぎ
- 1 意味区切り・2 文法区切り・3 品詞・4 音区切り・5 音のつながり(それぞれ、入り切り)。0 で、区切りを全部消します。ほかの表示も、アルファベットで入り切りできます(和訳 J・IPA I・文法 G・単語 V・TOEIC T・4択 Q・記録 K)。聞くは、スペース=全文、C=区切って、R=シャドーイング、L=音のつながり、D=聞き取り、S=止める。
- 以前の設定(見方を1つだけ選ぶ形)は、自動で移します。たとえば、「音で区切る」+「品詞の色オン」だった設定は、「音区切り」と「品詞」がオンの状態になります。
シャドーイングと聞き取り練習
英文の画面から、2つの音声練習を使えます。どちらも、端末の読み上げ機能を使うので、無料で、オフラインでも動きます。
シャドーイング
英文を、区切りごとに、「聞く → 同じ長さの無音(まねする時間)」で進めます。区切りは、**音区切り(IU)**があればIU、なければ文法区切り、意味区切り、の順で使います。IUは、息継ぎ・音の区切りなので、まねして声に出す単位に合います。
- 画面に「聞く(2 / 3)」「声に出して、まねしてください…(2.4秒)」と出ます。読んでいる区切りは、色が付きます。
- 間は、読み上げにかかった時間の 0.8 / 1 / 1.5 / 2倍から選びます(最短 1.2 秒)。繰り返しは、同じ区切りを 1〜3回です。
- 最後まで終わると、復習が1回増えます。途中で止めたときは、増えません。
- 意味句とIUのカードには、「聞いて、まねする」(その区切りだけを、聞いて、まねする時間を空ける)があります。
まねする時間は、読み上げに実際にかかった時間を測って決めます。声や速さによって、同じ文でも長さが変わるためです。測れないとき(読み上げが短すぎる、終了が通知されない)は、語数から見積もります。
function gap(spokenMs, text, mult, rate) {
const words = text.split(/\s+/).filter(Boolean).length;
const est = (words * 400) / (rate || 1); // 1語 0.4秒(速さで割る)
const base = spokenMs > 300 ? spokenMs : est;
return Math.max(1200, Math.round(base * mult + 400));
}
「止める」や、次の英文への移動で、進行中のシャドーイングを中断できるように、読み上げに世代番号を付けています。止めたり移動したりすると番号が増え、古い読み上げの続きは、何もせずに終わります。
聞き取り練習
英文を隠して、聞いて、書いて、語ごとに答え合わせをします。「聞く」「ゆっくり聞く(0.7倍)」で、何度でも聞けます。
答え合わせは、原文と、書いた文を、語の列として比べます(大文字・小文字と、語の前後の記号は区別しません)。最長共通部分列で、合っている語を決め、残りを次の3つに分けます。
| 表示 | 意味 |
|---|---|
| 緑 | 合っていた |
| 赤の取り消し線 | 聞き取れなかった(原文にあるが、書いていない) |
| 赤+小さい字 | 書き間違い(原文の語と、書いた語が、並んで食い違う) |
| 波線 | 余分(書いたが、原文に無い) |
「聞き取れなかった」と「余分」が並んだ所は、書き間違いとして1つにまとめます。たとえば、Candidates are advised to bring copies of their certificates to the interview. を、copies→copy・their→there・the 抜けで書くと、9 / 12 語になり、どこを間違えたかが分かります。
答え合わせをすると、復習が1回増えます。同じ聞き取りで何度押しても、増えるのは1回です。
確かめたこと
テスト用のブラウザには、読み上げの声が無いので、読み上げを差し替えて確かめました。区切り3つ×繰り返し2回で、読み上げが6回呼ばれること、途中で止めると復習が増えないこと、答え合わせの色分け、復習が1回だけ増えること、は確認済みです。実際の声での「まねする時間」の長さは、実際の端末で調整する前提です(そのために、間の倍率を選べるようにしています)。
音のつながり
聞き取りが難しい理由の1つは、英語では、語と語がくっついて聞こえることです。copies of は「コピゾヴ」のように1つの音に聞こえ、綴りと耳の音が結びつきません。そこを克服するために、「つながり」という見方を足しました。
IUとの違い
最初は、IU(一息で読む単位)を、そのための単位にするつもりでした。しかし、IUは、平均5語ほどのまとまりで、「2〜3語がくっついて聞こえる」所を見るには、長すぎます。そこで、IUは、シャドーイング用にそのまま残し、IUの中の、つながって聞こえる所を、別の見方として足しました。
| IU | つながり | |
|---|---|---|
| 何を表すか | 一息で読む単位(息継ぎ・抑揚) | くっついて聞こえる語のまとまり |
| 長さ | 2〜9語(平均5語) | 2〜4語 |
| 作り方 | AIに位置の番号を答えさせて作る | 発音辞書から、規則で作る(AIは使わない) |
| 使い道 | シャドーイング | 聞き比べ・穴あきの聞き取り |
つながりは、データとして保存しません。英文から、表示のたびに計算します。そのため、新しく入れた英文にも、すぐ付きます。
規則は、2つだけ
規則で確実に作れるものに絞りました。
- 連結:子音で終わる語に、母音で始まる語が続くとき、つなぐ。(
copies of→ˈkɑpiz‿əv) - 弱い語のくっつき:
of・to・the・a・and・inなど、弱く読まれる語が、次の語にくっつく。(to the interview.→tə ðə ˈɪntɚˌvju)
const linkable = (a, b) => !a.brk && a.ip && b.ip && !endsVowel(a.ip) && startsVowel(b.ip); // 連結
// 次の語につなぐのは、「連結できる」か「いまの語が弱い語」のとき。1つのまとまりは、最大4語
- 語の発音は、発音辞書から引きます。母音かどうかは、発音記号の、最初と最後の文字で決めます。
- 句読点(
,.など)をまたぐ所は、つなぎません(そこで、ふつう、間が入るためです)。 - 辞書に無い語があるまとまりは、つなぎません(途中が抜けた発音を出さないためです)。
- 発音記号の
‿(つながる所の印)は、連結のところに入れます。弱い語のくっつきは、空白のままで、下線で、まとまりを示します。
例(アプリの出力):
[Candidates are advised] /ˈkændədeɪts‿ˈɑɹ‿ædˈvaɪzd/
[to bring] /tə ˈbɹɪŋ/
[copies of their] /ˈkɑpiz‿əv ˈðɛɹ/
certificates
[to the interview.] /tə ðə ˈɪntɚˌvju/
samples の90文(1306語)では、2語以上のまとまりが312個できました。どの文でも、まとまりをつなぐと、原文に戻ります。
見る・聞く・書き取る
- 表示「音のつながり」:つながって聞こえやすいまとまりに下線を引き、その下に、つながりの印つきの発音記号を出します。
- 「音のつながり」を聞く:つながりのまとまりを、ゆっくり(0.7倍)→ふつうの速さの順に、聞き比べます。読んでいるまとまりが色付きになります。
- 聞き取り練習の「穴あき」:つながりのまとまりだけを空欄にして、聞こえた語を書きます。答え合わせでは、正解の綴りと発音記号を並べて見せ、間違えた所は、書いた答えも出します。「聞こえた音」と「綴り」の対応を、直接鍛える形です。答え合わせで復習が1回増え、間違いがあれば、「聞き取り」の間違いとして記録します。
確かめたこと
テスト用のブラウザには、読み上げの声が無いので、読み上げを差し替えて確かめました。例の文で、まとまりが4つ(Candidates are advised、to bring、copies of their、to the interview.)に分かれること、聞き比べで、各まとまりが「0.7倍→1倍」の順に読み上げられること、穴あきの空欄が4つできて、答え合わせで、正解・不正解が分かれることは、確認済みです。実際の声で、つながりが聞こえるかは、端末で試す前提です。
できないこと
- 脱落(
tが消える)、はじき音(tがdのように聞こえる)、want toがwannaになるなどは、規則が複雑なので、表せません。 - 辞書の発音は、強く読んだときの形です。
areやitにも、強勢の印が付きます。実際の会話では、もっと弱く読まれます。 - 読み上げは、端末の声です。声によって、つながり方が違い、実際の会話ほど、くっつかないことがあります。
単語の4択
英文の4択は、解析のときに、AIが作ります。単語の4択は、AIを使わず、単語の表から、機械的に作ります。単語は数が多く、1語ごとにAIを呼ぶと、時間も費用もかかるためです。また、誤答にする語は、手元の単語の表に、いくらでもあります。
問題の3つの形
| 形 | 問い | 選択肢 |
|---|---|---|
| 英語 → 意味 | 「complete」/kəmˈplit/ の意味は? | 意味を4つ |
| 意味 → 英語 | 「〜を修了する、完了する」を英語で言うと? | 単語を4つ |
| 空所に入る語 | 例文の中で、その単語を ______ にしたもの(意味をヒントに出す) |
単語を4つ |
形は、「自動」(作れる形から、ランダムに選ぶ)か、固定を選べます。
空所の問題は、例文の中に、その単語がそのまま出てくるときだけ作ります。単語は原形で持っているので、例文で promoted と活用していると、見つけられません(samples の329語のうち、空所を作れるのは221語です)。見つかる位置は、前後が英字でない所に限ります。たとえば、complete を探すとき、completed の途中には、当てはまりません。
誤答の選び方
誤答は、ほかの単語から選びます。
- 単語の表から、正解の単語を除き、ランダムに並べる
- 正解と同じ文字列(大文字・小文字を区別しない)は除く
- 品詞が同じものを先に、残りを後に並べ、先頭の3つを取る
- 3つに足りなければ、問題を作らない
品詞をそろえるのは、「動詞の意味」に、名詞の意味が混じると、品詞だけで答えが分かってしまうためです。単語が少ないうちは、誤答が足りず、問題を作れません。画面には、「誤答にする単語が、4語以上要ります」と出します。
選択肢の並びも、ランダムです。正解が1つだけで、4つの選択肢に重なりがないことを、samples の329語で、1語ごとに5回ずつ、問題を作って確かめました(不正な問題は0件)。
答えが見えないようにする
単語のカードには、答えが見えるものが、いくつもあります。単語そのもの、意味、例文です。4択を出している間は、答えるまで、これらを隠します。
- 「意味 → 英語」と「空所」:単語そのものが答えなので、カードの見出しを「?」にし、発音記号も隠す
- どの形でも:意味・例文・TOEICでの使われ方を隠す
答えたあとは、すべて表示し、「正解。『complete』=〜を修了する、完了する」と出します。
記録
- 答えると、復習が1回増えます
- 間違えると、分類「語彙」で記録します(単語にも、英文と同じ形の間違いの記録があります)
- 「記録」タブに、「単語の間違い N 語」と「間違えた単語を復習する」が出ます
- 学習対象選択で、単語も「間違いあり」で絞り込めます
受信箱から届いたデータでは、単語の間違いの記録も、取り込みません(英文と同じです)。
記録と復習
学習は、やりっぱなしにすると、何が身についたかが分からなくなります。そこで、「記録」タブで、状況・復習の記録・弱点・復習の始め方を見られるようにしました。試作の弱点集計を、4つの単位に広げたものです。
状態と復習回数
英文・意味句・IU・単語のすべてに、状態(未学習・学習中・覚えた。手動で切り替え、自動では変えません)、復習回数、最終復習日があります。復習は、次のたびに1回と数えます。
| 単位 | 復習1回と数えるとき |
|---|---|
| 英文 | 4択に答える/シャドーイングを最後まで終える/聞き取りの答え合わせをする |
| 意味句・単語 | 隠した意味(和訳)を見る |
| IU | 「練習した」を押す |
数える場所は、1つの関数にまとめました。回数と最終復習日を増やすのと同時に、日ごとの記録も残します。
async function countReview(r, kind) {
r.review_count++; r.last_reviewed = today(); await Store.put(r, kind);
const d = Stats.ymd(), e = reviewLog[d] || (reviewLog[d] = {});
e[KIND_UNIT[kind]] = (e[KIND_UNIT[kind]] || 0) + 1; // 日ごとの回数(英文・単語・区切り)
}
日ごとの記録(今日・直近7日・14日の回数、連続日数、棒グラフ)は、この端末だけのものです。同期はしません。状態・復習回数・間違いは、英文などの行に付いているので、同期されます。
間違いの記録
間違いは、分類・日付・どこで間違えたかを、英文に持たせます。
- 4択で間違えたとき:問題の論点(時制・前置詞・関係詞など)を分類にして、自動で記録します。試作は、間違えるたびに分類を選ばせていましたが、4択には、解析のときに論点が付いているので、選ぶ手間を省きました。
- 聞き取りで間違えたとき:書き間違い・聞き取れなかった語・余分な語のどれかがあれば、分類「聞き取り」で記録します。
- 手動:英文の「間違いの記録」で、分類を選んで追加できます。×で消せます。
分類は、4択の論点(13種)に「聞き取り」を足した14種に決めています。外から来た値は、この14種に当てはまらなければ「その他」にします。受信箱から届いたデータでは、間違いの記録は取り込みません(状態や復習回数と同じく、外から書き換えられないようにするためです)。
弱点の集計
間違いの分類ごとに、件数を数えます。期間は、直近7日・30日・全期間です。分類を押すと、その分類の英文の一覧が出て、「この分類の英文を復習する」で、そのまま学習を始められます。集計は、画面に依らない関数にして、日付を決めて試験しました。
function weak(rows, days) { // days = 直近何日か(null なら全期間)
const since = days ? ymd(addDays(-(days - 1))) : null;
// 間違いごとに、分類 → 件数と英文の id を集計。多い順
}
復習の始め方
「覚えた」を除き、復習回数の少ない順に選びます。同じ回数なら、最後に復習した日が古い順で、一度も復習していないものが先です。単位と件数を選ぶほか、「間違えた英文を復習する」(選んでいる期間に間違いのある英文)もあります。
function reviewOrder(list, n) {
// 覚えた以外 → 復習回数の少ない順 → 最終復習日の古い順(未復習が先)→ 元の並び
}
直したこと:日付が前日になっていた
最終復習日を、new Date().toISOString() の先頭10文字で作っていました。これはUTCの日付です。日本では、午前9時までは、前日の日付になります。朝に復習すると、昨日の復習になり、連続日数も狂います。端末の日付(年・月・日を、手元の時刻から作る)に直しました。
個別削除
登録した英文・単語・意味句・IUを、1件ずつ(または選んでまとめて)消せます。
- 学習対象選択:チェックを付けて「選んだ N 件を削除」。
- 学習中のカード:英文の画面の「この英文を削除」、単語・意味句・IUのカードの「削除」。
- 消す前に、確認のダイアログで、何を消すかを見せます(件数、同期している端末からも消えること、元に戻せないこと)。
英文を消すと、意味句とIUも消える
意味句とIUは、英文から取り出して作った行です(親の英文の文を、ex に持っています)。親の英文だけが消えて、意味句とIUが残ると、「文の中で見る」が、元の英文を指さなくなります。そこで、英文を消すと、その英文から作った意味句・IUも、いっしょに消します。確認のダイアログには、その個数も出します(「意味句・IU(14 個)も、いっしょに削除します」)。
単語は残します。単語は、同じ単語が、ほかの英文でも使われるためです。単語を消したいときは、単語として消します。
if (u === "sentence") {
const texts = new Set(rows.filter(r => set.has(r.id)).map(r => r.text.toLowerCase()));
cids = chunks.filter(c => texts.has(c.ex.toLowerCase())).map(c => c.id); // 親の英文から作った区切り
// …英文と、その区切りを、表から消す
}
消したものは、学習セット・選択の状態からも外します。最後の1件を消すと、学習の画面は、「学習する英文が選ばれていません」に戻ります。
同期している端末からも消す
削除は、同期で、ほかの端末にも伝えます。同期は、前回の同期から変わったものだけを送ります。そのため、「前回は、手元にあった行が、いま無い」ことが、「消した」という変更になります。消した行は、中身を持たない「消した」という印(削除の記録)として送ります。
試験で、消した行が、同期で復活する不具合を見つけました(「作るなかで直したこと」に書きました)。
暗号化したクラウド同期と受信箱
設計は、試作と同じです。パスフレーズから鍵を作り(PBKDF2・60万回)、英文・単語・意味句・IUを AES-GCM で暗号化してから Cloudflare D1 に保存します。サーバーは暗号文しか持たないので、中身は読めません。2台目は、同期キー(またはQRコード)とパスフレーズで接続します。変更は、前回の同期から変わったものだけを、数秒後に送ります。
試作から変えた点が、1つあります。暗号鍵を、取り出せない形(extractable: false)で、この端末の IndexedDB に置くようにしました。試作は、鍵を文字列にして localStorage に置いていました。
受信箱
Claude Code は、利用者のデータに直接書き込めません。そこで、書き込み専用の合言葉で、内容を公開鍵(RSA-OAEP)で暗号化して預けます。復号できるのは、パスフレーズを知る端末だけです。受け取るときは、使う項目だけを取り出し、状態・復習回数は取り込みません(外から「覚えた」にはできません)。同じ英文・単語・区切りは、足りない項目だけを足します。
ローカルの Worker と D1 で、次を確かめました。保管庫の作成、受信箱の合言葉の発行、inbox-send.mjs での送信、同期での受け取り、別の端末での接続。「覚えた」にしようとした英文は未学習のまま入り、すでにある発音記号は上書きされず、手元にだけあった同じ英文は1つにまとまりました。
作るなかで直したこと
- 並び順:貼り付けた英文が、再読み込みのたびに入れ替わりました。id が、時刻と乱数でできていて、同じ瞬間に作ったものの順が決まらなかったためです。id に連番を入れ、作った順に並ぶ形にしました。
- 空の和訳:和訳が空文字のとき、「足りない」と見なされず、足されませんでした。空文字も、無いものとして扱うようにしました。
- 同期で戻ってくる自分の変更:送ったものが同期で戻ってきたとき、受信として数えていました。内容が同じなら、数えないようにしました。
- 消した行が、同期で戻っていた:同期は、送る前に、サーバーから新しい行を受け取ります。このとき、自分が前に送った行も、返ってきます。手元で消した行は、手元に無いので、「新しい行」と見なされ、復活しました。前回までに同期した行が、手元に無いときは、「この端末で消した」と判断し、戻さないようにしました。
- 別の端末に、一部の行が届かなかった:2台目の端末で接続すると、単語 281 語(正しくは 329 語)など、一部が届きませんでした。サーバーは、1回の送信でまとめて送られた行に、同じ番号(
seq)を付けます。取得は、300行ずつ、「番号が前回より大きいもの」を返しますが、ページの区切りが、同じ番号の行の途中に来ると、続きの行が、次の取得で飛ばされます。区切りの先頭の行と同じ番号の行は、そのページに入れず、次のページに回すようにしました(1回の送信は200行までで、ページより小さいので、必ず進みます)。直したあと、2台目に、全部の行が、同じ件数で届き、削除も反映されました。 - 受信箱から受け取っても、何も出なかった:受信箱の英文が、すでに登録済みだと、重複として除かれ、何も追加されません。このとき、何も表示していなかったので、「届かない」ように見えました。受け取った通数は、必ず表示する(「新しいものは、ありませんでした」も含む)ようにし、同期の画面に、「前回の受け取り」(日時・通数・追加した数)も残すようにしました。
限界
- 意味句・IUの発音記号は、語の発音をつないだものです。弱い発音や音のつながりは出ません。辞書に無い語(数字・固有名詞など)を含む区切りには、付きません。
- AIの解析は、検査を通ったものだけを入れますが、内容の正しさまでは検査できません。区切りの形のラベル、文法の説明、IUの区切り位置などは、誤りが混じりえます。入れた英文を「未確認」にしているのは、そのためです。
- 品詞の色は、文字の色だけで表します(以前は、色と下線の両方でした)。色の違いが見分けにくい人のための、「品詞名も表示」(各語の下に品詞名を出す)は、まだありません。
- つながりは、規則で作れる連結と弱い語のくっつきだけです(脱落・はじき音などは表せません)。辞書の発音は強く読んだ形なので、実際の会話より、強勢が多く出ます。
- 削除は、元に戻せません。同期している端末からも消えます。同じ英文を、受信箱・ファイル・サンプルで、もう一度入れれば、復活します。
- 単語の4択の「空所」は、例文に単語がそのまま出てくるときだけ作れます(活用した形は見つけられません)。日ごとの復習の記録は、この端末だけのもので、同期されません。
- APIの呼び出しは、返事を差し替えた試験までで、実際の Gemini の返事での確認は、これからです。