跳到正文
DEV Community· Low Key Builds·· 3 小時前AI 評分56

Sunlo 免費轉錄烏爾都語語音訊息,提供 Roman Urdu 並列出辨識錯誤率

Urdu voice notes to text on a free tier: Whisper pinned to Urdu, Roman Urdu without drift, and an honest error rate

AI 導讀

Low Key Builds 製作了 Sunlo 這款免費網頁應用程式,可將 WhatsApp 語音訊息轉成烏爾都文、Roman Urdu 及英文翻譯,免帳戶使用並支援最長 10 分鐘錄音。

正文

Pakistan runs on WhatsApp voice notes. WhatsApp now writes transcripts for some languages, but Urdu is not in any list of supported languages I could find, and a Hindi transcript of Urdu speech comes out in Devanagari, which most Pakistanis do not read.

So I built Sunlo, a free web app: drop in a voice note, get Urdu script, Roman Urdu (the way people actually type on WhatsApp) and an English translation. Disclosure: I made it, and this post is how it works, including what it gets wrong.

The constraints: no budget, no audio stored anywhere, and a cheap Android phone as the main client.

1. Whisper on Workers AI, language pinned to Urdu

Cloudflare Workers AI hosts whisper-large-v3-turbo. The one setting that matters is language: 'ur'. Left to auto-detect, Whisper often decides Urdu speech is Hindi (the spoken languages are close) and writes Devanagari.

const w = await env.AI.run('@cf/openai/whisper-large-v3-turbo', {
  audio: base64Audio,      // WhatsApp .opus works as-is
  language: 'ur',          // never auto-detect for Urdu
  task: 'transcribe',
  vad_filter: true,        // skips silence in voice notes
});

WhatsApp's .opus files go straight in; no ffmpeg needed on the server.

2. A hard daily cap, enforced before the model runs

The free Workers AI allocation is 10,000 "neurons" a day. Whisper turbo costs about 46 neurons per audio minute ($0.00051/min at $0.011 per 1k neurons). The Worker keeps a per-day counter in D1 and reserves the estimated cost before calling the model, so parallel requests cannot overshoot, then corrects it with the real duration afterwards:

const est = seconds / 60 * 46.4 + 40;
const b = await env.DB.prepare('SELECT neurons FROM budget WHERE day=?').bind(day).first();
if ((b?.neurons || 0) + est > 8000) return json({ error: 'daily_cap' }, 503);
await env.DB.prepare(`INSERT INTO budget (day, neurons) VALUES (?, ?)
  ON CONFLICT(day) DO UPDATE SET neurons = neurons + excluded.neurons`).bind(day, est).run();

Per-visitor limits use a salted daily hash of the IP, so nothing identifying is kept. Audio and text are never written anywhere; D1 only holds counts, durations and timings.

3. Roman Urdu: transliterate, do not translate

Roman Urdu and English come from one call to Llama 4 Scout (about 11 neurons for a short note). The first prompt said "write it in Roman Urdu" and the model drifted into English words ("patient ko dusre se alag karna include hai"). Saying TRANSLITERATE, with two examples of how people type, fixed it:

"roman": TRANSLITERATE, do not translate. Write every Urdu word as the same
Urdu word in Latin letters, the way Pakistanis type on WhatsApp
(for example "main kal aaunga, aap fikar na karein").

Llama 3.1 8B was cheaper but changed words ("aaj", today, became "kal"). Scout, Llama 3.3 70B and gpt-oss-120b were all fine on clean text.

4. Long recordings: cut at silence, in the browser

Lectures and interviews are longer than one request should be. The browser decodes the file with AudioContext({ sampleRate: 16000 }), which also resamples to what Whisper wants, then cuts it into ~150-second pieces. Each cut goes at the quietest 50 ms window in the last quarter of the piece, so words are not split:

for (let w0 = lo; w0 < hi; w0 += win) {
  let e = 0;
  for (let j = w0; j < w0 + win; j++) e += mix[j] * mix[j];
  if (e < bestE) { bestE = e; best = w0 + (win >> 1); }
}

Pieces are transcribed one by one, joined, and then translated in a single pass, because translating fragments separately produced nonsense at the seams.

5. Share target: one tap from WhatsApp

On Android, an installed PWA can appear in WhatsApp's Share sheet. The manifest declares a share_target with a multipart POST; the service worker catches it, stashes the file in Cache Storage and redirects to the page, which picks it up:

"share_target": { "action": "/share-target", "method": "POST",
  "enctype": "multipart/form-data",
  "params": { "files": [{ "name": "audio", "accept": ["audio/*", ".opus", ".ogg"] }] } }

How accurate is it?

I ran the first 30 clips of the FLEURS Urdu test set (28 downloaded) through the live API, encoded as Opus like a WhatsApp note:

Condition Word error rate Character error rate
Clean 20.8% 8.5%
White noise at 15 dB SNR 23.0% 9.3%

That counts spelling variants and "70" against "ستر" as errors, so the text reads better than one wrong word in five suggests. Real voice notes are harder than read sentences.

The failure I worry about most: in a noisy test, "jald rihaa" (released quickly) was heard as "jal raha" (burning), and every model I tried, including the 70B and 120B ones, then translated the sentence as hostages being set on fire. A recognition slip becomes an alarming English sentence. The prompt now tells the model not to invent dramatic meanings, but the honest fix is in the interface: every result says it is a machine transcript and to check money, medicine, addresses and times against the audio. The method and examples are on the accuracy page.

Try it

sunlo.appin60.com is free, needs no account, and handles notes up to 10 minutes. If you speak Urdu, I would like to hear where it goes wrong: sunlo@appin60.com.

來源:DEV Community · dev.to