この記事で使う TonePerfect API は私が作っているサービスです。宣伝のためではなく、「JavaScript から日本語の発音を採点するとどう見えるか」を、実際に叩いた結果と、うまくいかないケースも含めて書きます。
やること
- 学習者が「決まった文」を読み上げた録音を、Node.js からAPIに送って採点する
- 文全体のスコアと、文節ごとのスコア・タイムスタンプ・「直すべきか」の判定を受け取る
- 別の文を読んだときにどう見えるかも試す(ここが一番の学び)
対象は 台本ありの音声(scripted speech) だけです。音声認識(何を言ったかの書き起こし)ではありません。
前提
- Node.js 18 以上(グローバルの
fetch/FormData/Blobを使います) - APIキー:https://api.toneperfect.app/signup で作成(新規アカウントに無料50クレジット、カード不要)
- テスト用の音声ファイル(WAV / MP3 / M4A / Ogg / WebM、0.15〜30秒、10MBまで)
手元に学習者の録音がない場合、配線の確認だけなら macOS の読み上げで作れます。
say -v Kyoko "日本語を勉強しています" -o ja.aiff
ffmpeg -y -i ja.aiff -ar 16000 -ac 1 ja.wav
TTS はネイティブ寄りの音なので高めに出ます。あくまで動作確認用です。
1. まず capabilities を見る(キー不要)
GET /v1/capabilities で、日本語パックが使えるか・何を採点するか・既知の限界は何かが返ってきます。採点結果を画面に出す前に、ここに書いてある限界を読んでおくのが大事です。
const caps = await (await fetch('https://api.toneperfect.app/v1/capabilities')).json();
const ja = caps.languages.find((l) => l.locale === 'ja-JP');
console.log(ja.status, ja.dimensions, ja.limitations);
2026年10月7日時点の出力(抜粋):
experimental [ 'pronunciation', 'completeness' ]
New pack (beta): native-reference scale, not yet calibrated on graded learner recordings;
a different sentence can still score around 30, and kanji are read with a dictionary that
can miss context-dependent readings (今日は). Text is split into bunsetsu-like units;
pitch accent is not scored.
つまり:
- 日本語はベータ(
experimental) - 採点の軸は「発音」と「完全性(読み落としがないか)」
- ピッチアクセントは採点しない
- 漢字の読みは辞書ベースなので、「今日は」のように文脈で読みが変わるものは、かなで渡したほうが安全
- 単位は単語ではなく「文節っぽい単位」(日本語を / 勉強しています)
2. 採点スクリプト
// assess-ja.mjs
// Node 18+ / 実行: TONEPERFECT_API_KEY=tp_live_... node assess-ja.mjs ja.wav "日本語を勉強しています"
import { readFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
const API = 'https://api.toneperfect.app';
const [file = 'ja.wav', text = '日本語を勉強しています'] = process.argv.slice(2);
// 1) 日本語パックが使えるか確認(キー不要)
const caps = await (await fetch(`${API}/v1/capabilities`)).json();
const ja = caps.languages.find((l) => l.locale === 'ja-JP');
if (!ja?.available) throw new Error('ja-JP is not available right now');
console.log(`ja-JP: ${ja.status}, dimensions=${ja.dimensions.join(',')}, max ${ja.max_duration_seconds}s`);
// 2) 録音と「読むはずだった文」を送る
const form = new FormData();
form.append('audio', new Blob([await readFile(file)], { type: 'audio/wav' }), file);
form.append('text', text);
form.append('language', 'ja-JP');
const res = await fetch(`${API}/v1/assess`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TONEPERFECT_API_KEY}`,
// 録音1回につき1つ。同じ録音のリトライでだけ使い回す
'Idempotency-Key': `ja-${randomUUID()}`,
},
body: form,
});
const r = await res.json();
if (!res.ok) throw new Error(`${res.status} ${r.error?.code}: ${r.error?.message}`);
// 3) 無音などで採点できない場合も 200 で返り、assessable=false になる(課金されない)
if (!r.assessable) {
console.log(`not assessable: ${r.reason}`);
process.exit(0);
}
console.log(`overall ${r.scores.overall} pronunciation ${r.scores.pronunciation} completeness ${r.scores.integrity}`);
for (const w of r.words) {
// 日本語の words は文節っぽい単位。decision は review / uncertain / no_correction
const flag = w.decision === 'review' ? ' <- 要確認' : '';
console.log(`${w.word}\t${w.score}\t${w.start_ms}-${w.end_ms}ms\t${w.decision}${flag}`);
}
console.log('credits charged:', r.billing.credits, 'free left:', r.billing.free_credits);
ポイント:
-
Content-Typeは自分で付けない。FormDataを渡せばfetchが boundary 付きで設定してくれます。 -
Idempotency-Keyは必須です。ネットワークエラーで同じ録音を送り直すときだけ同じキーを使えば、二重課金されません。 - APIキーはサーバー側に置きます。ブラウザやモバイルアプリから直接呼ぶのではなく、自分のバックエンド経由にしてください。
3. 実行結果(読んだ文と一致する場合)
ja-JP: experimental, dimensions=pronunciation,completeness, max 30s
overall 83.1 pronunciation 90.6 completeness 91.6
日本語を 95.5 0-676ms no_correction
勉強しています 86.6 676-2016ms no_correction
credits charged: 1 free left: 49
文節ごとに start_ms / end_ms が付いているので、「この部分をもう一度聞く」「ここだけハイライト」といったUIが作りやすいです。
score(0〜100)は表示用のスコアで、「ネイティブらしさ何%」という意味ではありません。学習者に「ここを直して」と言うかどうかは、スコアの閾値ではなく decision を見て決めるのがこのAPIの想定です(review のときだけ指摘する、など)。
4. 別の文を読んだらどうなるか
ここが一番面白かったところです。録音は「ちょっと待ってください」、text は「日本語を勉強しています」のまま送ってみました。
overall 34.4 pronunciation 54.1 completeness 66.2
日本語を 85.1 0-445ms no_correction
勉強しています 64.9 445-1435ms no_correction
全体スコアは 34.4 まで下がります。ただ、「日本語を」単体は 85.1 と高めに出ています。capabilities の「a different sentence can still score around 30」という注意書きどおりで、ベータの日本語パックでは、文節単位のスコアだけ見ると「読んでいない部分がそこそこ良い」ように見えることがあります。
なので実装するときは:
- まず
assessableを見る - 次に
scores.overallとscores.integrity(completeness)で「そもそも指定した文を読んだか」を判定する(閾値は自分のデータで決める) - そのうえで文節ごとの
decisionを出す
という順番にしておくのが安全だと思います。単語・文節のスコアだけで「よくできました」を出すのは避けたほうがいいです。
5. 日本語で使うときの注意点まとめ
- ピッチアクセント(高低)は採点しません。採点するのは子音・母音・長さなどの音です。
- 漢字の読みは辞書ベース。文脈で読みが変わる語は、かなで渡す。
- 数字は文字で書く(「3人」ではなく「さんにん」)。
- 1回の録音は30秒・10MBまで、15秒までなら1クレジット、30秒までなら2クレジット。
- 無音や採点不能(
assessable: false)は課金されません。 - 音声は採点後に削除され、学習には使われません。
おわりに
日本語パックはまだベータで、学習者の録音で校正しきれていません。「別の文でも30前後出る」「辞書読みが外れる」といった限界は capabilities とレスポンスにそのまま出るようにしているので、使う側で判定順序を工夫するのが現実的です。
TonePerfect API(作者は私です): https://api.toneperfect.app / ドキュメント: https://api.toneperfect.app/docs / 新規アカウントに無料50クレジット。おかしな採点結果があれば nik@toneperfect.app まで送ってもらえると助かります。