0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptで日本語の発音を採点する:TonePerfect APIの使い方と限界

0
Posted at

この記事で使う 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 まで送ってもらえると助かります。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?