today's news, the way it is spoken

A free web app for learning spoken Cantonese

Today's Hong Kong news, rewritten the way an anchor would say it.

Written Chinese and spoken Cantonese differ in vocabulary and grammar: 認為 on the page is 覺得 out loud. Every morning a GitHub Actions job pulls RTHK stories, converts each sentence into spoken Cantonese, adds jyutping over every character and records neural zh-HK audio. The conversion is meant to be an LLM rewrite with a second review pass, but the build logs show it has fallen back to the rule-based converter on all 95 builds GitHub still keeps, since 7 August 2026. The app shows the written and spoken versions side by side and grades you character by character when you read aloud. It also has 96 scenario dialogues, from a cha chaan teng breakfast to an earnings call.

WrittenSpoken, with jyutping
Waiter 早上好,幾位? 早zou2晨san4,幾gei2多do1位wai6呀aa3? Morning, how many?
Waiter 這邊坐。想吃什麼? 呢ni1邊bin1坐zo6啦laa1。想soeng2食sik6咩me1呀aa3? Sit over here. What would you like?
Customer 牛油就可以了。 牛ngau4油jau4就zau6得dak1喇laa3。 Butter is fine.

Lines 1, 3 and 8 of the 11-line "Breakfast at a cha chaan teng" scenario, copied from frontend/data/conversations.json. 這邊 becomes 呢邊, 吃什麼 becomes 食咩, and 可以了 becomes 得喇.

96

scenario dialogues, 982 lines, in 8 categories of 12, each line with a speaker label, English gloss and its own neural audio.

2 passes

per news sentence as designed: a GPT-4.1 rewrite, then a GPT-4.1-mini review. Neither has run in the 95 logged builds; see below.

$0

to run: GitHub Models with the workflow's own token, edge-tts for audio, GitHub Pages for hosting. A Claude key is optional.

06:00

Hong Kong time, when the daily build fetches, rewrites, voices and republishes the lessons.

At a glance

Task
Rewrite about 12 news stories a day, sentence by sentence, from written Chinese into spoken Cantonese
Evidence
The logs of all 95 daily builds GitHub keeps, 7 August to 5 November 2026 (eval/build_logs/). No scored set of rewrites exists
Baseline
The rule-based converter, the bottom rung of the ladder
Result
The LLM tier ran on 0 of 95 builds; every build shipped the rule-based conversion, and today.json says so
Cost per article
$0: no LLM call succeeded. The LLM tier's cost and latency could not be measured, because its endpoint no longer answers
Latency
The whole build step for 12 articles (fetch, convert, jyutping): 58 s median, 112 s slowest; per article it is not logged
Review catch rate
Unknown. The review pass never ran in a kept log, and the pipeline does not record what a review changes
Main failure
A silent fallback. Until 20 September both models returned HTTP 410; since 22 September the endpoint answers 200 with the plain text "OK", the JSON parse throws, and the code returns null without logging

The problem

You can read the paper and still miss the evening news

In Hong Kong you read one register and hear another: newspapers are in written Chinese, while the evening news is spoken in Cantonese, with different words and different grammar. A learner can manage the first and still lose the thread of the second. The app puts both on screen and makes the mapping between them the thing you study.

The reader on a Bain Capital news story: written Chinese on the left, spoken Cantonese with jyutping above every character on the right, 4% spelled out as 百分之四, and a tap-to-compare hint below.
The reader on a news story: written on the left, spoken on the right, with 4% read out as 百分之四.

Tap to compare

Tap a phrase and its counterpart lights up

The rewrite produces aligned phrase pairs. Tap an underlined phrase in either pane and its counterpart highlights in the other, so you can see that 認為 became 覺得. With the free tier, the pairs come from a character-level longest-common-subsequence aligner; the Claude tier returns them directly in its structured output.

On a phone the two panes become an interleaved view, with each written line directly above its spoken version.

The same reader in the dark theme, with the written and spoken panes side by side and jyutping above every character.
The reader in the dark theme.

Quality

A rewrite ladder with a review pass

Mechanical written-to-spoken conversion makes errors a learner can't spot (single-character rules turn 不足 into 唔足 and 其他 into 其佢), so the build uses the best tier available:

Claude
With an ANTHROPIC_API_KEY secret: a rewrite that returns aligned phrase pairs, then an independent verifier call. The day is labeled llm+verify
GitHub Models
The default, free, and not working since at least August 2026 (next section): GPT-4.1 with a few-shot prompt that asks for restructured spoken Cantonese, falling back to GPT-4.1-mini when the daily quota runs out, then a GPT-4.1-mini review pass that may not make a line less colloquial. Calls are serialized with back-off and a per-model quota circuit breaker
Rules
The last resort, rebuilt after an audit of real output: phrase whitelists in place of single-character swaps (不 to 唔, 這 to 呢), quoted names in 「」 and 《》 left alone, sentence-final 了 to 喇

What the build ran

95 builds, 95 rule-based conversions

scripts/build-lessons.mjs prints one summary line per build, such as "Wrote today.json: 12 articles, 282 sentences (rewrite: rules)", and one line per failed GitHub Models call. eval/build_logs/scan.py reads that line from every build log GitHub still keeps:

7 Aug to 20 Sep 202646builds where GPT-4.1 and GPT-4.1-mini both returned HTTP 410 on all 12 articles; rules used
22 Sep to 5 Nov 202649builds with no error line and rules used: the endpoint now replies 200 with a plain-text "OK", which the parser rejects
Used the LLM tier0of 95

The "OK" reply was reproduced on 2026-11-05 with a request shaped like the pipeline's (saved in eval/build_logs/summary.json). chat() in backend/ghmodels.js catches every exception and returns null, and the ladder then drops to the rules, so the site kept publishing and nothing flagged the change. The lessons are labeled honestly: each day's today.json carries "method": "rules" and the note "Auto-converted to Cantonese (rule-based, rough)". So the news lessons on the live site are the rule converter's output, and the review pass described above has had nothing to review.

Numbers

Every numeral spelled out in context

In V1, Arabic numerals had no jyutping and no reliable reading. V2's numbers.js, shared by the build and the browser, reads each number the way an anchor would. The grader runs the recognizer's output through the same engine, so a correct reading scores green whether Chrome hears 二零二零年 or 2020年.

2020年二零二零年a year, digit by digit
2020個二千零二十個a quantity, as a cardinal
15%百分之十五a percentage
54.1五十四點一a decimal
28463222二八四六三二二二a long digit run, like a phone number

Output of spellOutNumbers() from frontend/numbers.js, run 2026-10-26.

This box imports numbers.js from the live app and calls the same spellOutNumbers() the daily build and the grader use.

The curriculum

Eight categories of conversations you will have

Next to the news are 96 dialogues in 8 categories of 12, all browsable below: eating and drinking, getting around, shopping, friends and small talk, office life, finance and deals, home and services, and health and weather. Each line has written Chinese, spoken Cantonese with jyutping, an English gloss, a speaker label, and audio in a different voice per speaker. The source is hand-authored in content/conversations-src.json and was checked by separate AI review passes for natural Cantonese and for agreement between the three layers. The build script enforces 12 scenarios per category, no digits, and no Cantonese-only characters in the written layer.

Loading the dialogues…

Read straight from the app's data/conversations.json when this page loads: written Chinese, spoken Cantonese with the stored jyutping, and the English gloss. In the app each line also has its own voice and a record button.

Architecture

No server: a daily build and static files

GitHub Actions is the cron job and the build server. At 06:00 Hong Kong time it fetches RTHK RSS across five topics and the full article bodies, rewrites and reviews them, spells out numbers, adds jyutping with the to-jyutping library, synthesizes an MP3 per sentence with edge-tts zh-HK voices, and publishes frontend/ to GitHub Pages. Every step fails soft: no key means rules, a failed fetch means bundled sample lessons, a failed clip means the browser voice for that sentence.

Up to four lesson slots go to a second source: a WeChat RSS bridge if one is configured, otherwise a scrape of Google News for Chinese-language Bain Capital coverage. backend/junk.js filters reload banners, disclaimers and paywall prompts, and an article left with nothing but boilerplate is dropped. The repo has unit tests with node:test and a Playwright end-to-end test that renders the app headless.

Limitations

Chrome for grading, and a machine in the loop

Grading uses the browser's Web Speech API with the yue-Hant-HK locale, so it needs Chrome; other browsers can listen and read. In Chrome that API sends the recording to Google's speech service for recognition.

The spoken Cantonese in news lessons is machine-written. Since at least August 2026 it has come from the rule converter alone, which swaps phrases and does not restructure grammar, so it reads closer to written Chinese than a real anchor would. Treat it as practice material, and trust your ear when a line sounds wrong.

News lessons are rebuilt every day and not archived. Space-separated digit groups such as a phone number written "2846 3222" are currently read as two cardinals; only unbroken runs are read digit by digit.

Built with

Free to use, free to run

Build
Node.js on GitHub Actions; RSS and article extraction, GitHub Models (GPT-4.1, GPT-4.1-mini) or the Anthropic SDK with structured outputs
Language
to-jyutping for jyutping, OpenCC for Traditional and Simplified, a character-LCS phrase aligner
Audio
edge-tts zh-HK neural voices, pre-rendered per sentence; pitch-preserving speed control in the browser
Front end
Static HTML, CSS and JavaScript; English, Traditional and Simplified interface
Tests
node:test unit tests and Playwright end-to-end
Sibling
Mandarin Learning Reader, the same listen, speak and grade loop over any Chinese e-book

Learn the Cantonese people speak