Blog

10. Oktober 2026 · 6 min read

Your first pronunciation score in five minutes

The official Lingolix SDKs for Python and TypeScript wrap one endpoint with typed results and typed errors. This guide takes you from install to a feature your learners can use.

The Python and npm logos on either side of the Lingolix logo

1. Get a key

Create an API key in the API portal and put it in your environment. Both SDKs read it from there.

shell
export LINGOLIX_API_KEY=your-key

Keep the key on your server. Anyone who has it can spend your quota, so it never belongs in a mobile app or a web page. Section 4 shows the usual setup.

2. Install

Python 3.10+
pip install lingolix
Node 18+, Bun, Deno, Workers
npm install @lingolix/sdk

The TypeScript client has no runtime dependencies: it uses the platform's fetch, FormData and Blob.

3. Score a recording

Send the audio and the sentence the learner was asked to say. Any format ffmpeg reads works: WAV, MP3, M4A, Ogg/Opus, FLAC, WebM, up to 10 MiB.

Python
from lingolix import Lingolix

with Lingolix() as client:
    r = client.check("hello.m4a", sentence="Guten Morgen", language="de")

print(f"{r.overall:.0%}")
for word in r.words:
    for s in word.syllables:
        print(s.text, s.expected_ipa, "→", s.detected_ipa, f"{s.accuracy:.0%}")
TypeScript
import { readFile } from "node:fs/promises";
import { Lingolix } from "@lingolix/sdk";

const client = new Lingolix();
const r = await client.check({
  audio: await readFile("hello.m4a"),
  sentence: "Guten Morgen",
  language: "de",
});

console.log(Math.round(r.overall * 100) + "%");

Python has an async twin, AsyncLingolix, with the same methods. Leave out sentence and the audio is transcribed first and scored against what was said, which suits free speaking practice.

What comes back

overallThe score to show, 0–1. Accuracy and fluency combined.
accuracyHow well the sounds were pronounced, 0–1.
fluencyPace and pauses, 0–1. null for very short utterances; then overall equals accuracy.
completenessHow much of the sentence was actually said, 0–1.
speaking_rateSyllables per second (Japanese: morae), next to target_speaking_rate.
words[]Per word: accuracy, timing, hesitation_ms, and its syllables.
syllables[]Per syllable: expected_ipa vs detected_ipa, accuracy, pitch, timing, is_missing, is_extra.
Example result (abridged)
{
  "text": "I would like a cup of coffee, please.",
  "overall": 0.86, "accuracy": 0.84, "fluency": 1.0, "completeness": 1.0,
  "speaking_rate": 4.1, "target_speaking_rate": 2.77,
  "model_version": "2026-10",
  "words": [
    { "text": "coffee,", "accuracy": 0.62, "hesitation_ms": null,
      "syllables": [
        { "text": "cof", "expected_ipa": "kɔ", "detected_ipa": "kɑ", "accuracy": 0.48, "pitch": "flat" },
        { "text": "fee,", "expected_ipa": "fi", "detected_ipa": "fi", "accuracy": 1.0, "pitch": "low" }
      ] }
  ]
}

Turning it into feedback

A pattern that works well with learners:

  1. Show overall as the headline score. It is the number that best matches how a teacher would judge the whole sentence.
  2. Colour each word by its accuracy, and let learners tap a word to see its syllables.
  3. For a weak syllable, show expected_ipa next to detected_ipa: “you said kɑ, aim for kɔ” is more useful than a percentage.
  4. Mark a word with hesitation_ms as a pause, and compare speaking_rate with target_speaking_rate for a gentle “try a little faster”.

4. Record in the browser, score on your server

The usual setup: the browser records with MediaRecorder, sends the clip to your backend, and your backend calls Lingolix with the key. A Next.js route handler is all it takes:

app/api/score/route.ts
import { Lingolix } from "@lingolix/sdk";

const client = new Lingolix(); // reads LINGOLIX_API_KEY on the server

export async function POST(req: Request) {
  const form = await req.formData();
  const r = await client.check({
    audio: form.get("audio") as Blob,
    sentence: String(form.get("sentence")),
    language: "en",
  });
  return Response.json(r);
}
In the browser
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
const chunks: Blob[] = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = async () => {
  const form = new FormData();
  form.append("audio", new Blob(chunks, { type: recorder.mimeType }));
  form.append("sentence", "I would like a cup of coffee, please.");
  const result = await (await fetch("/api/score", { method: "POST", body: form })).json();
  // show result.overall, colour result.words ...
};
recorder.start();

Compressed audio keeps uploads small: a sentence of WebM/Opus from MediaRecorder is a few dozen kilobytes.

5. When things go wrong

Every failure is a LingolixError with status and detail, and each cause has its own subclass, so you can handle the ones that matter to your users:

Python
from lingolix import Lingolix, InvalidAudioError, QuotaExceededError

try:
    r = Lingolix().check("clip.webm", sentence="Bonjour", language="fr")
except InvalidAudioError:
    ...  # ask the learner to record again
except QuotaExceededError as e:
    ...  # e.plan, e.remaining_minutes, e.upgrade_url

The others are AuthenticationError (401), SubscriptionError (403) and ServiceUnavailableError (503, worth one retry).

6. Languages

Pass the ISO code as language: bg cs de el en es fi fr hu it ja ko nl pl pt sv uk. Japanese results come back per mora in hiragana, with pitch per mora.

Next steps

Get an API key