Skip to content

Releases: tgeczy/panthera-speech

tiger-speech 0.7.8 — a crash Tiger never had, and a report that names itself

Choose a tag to compare

@tgeczy tgeczy released this 20 Aug 01:48

Both fixes in this release were found on Leopard, and measurement says Tiger has neither of the faults. It is still worth installing, and the honest summary is below.

The crash Tiger does not have

"x" * 20 + "the" killed the Leopard host ten times out of ten — issue #4, found by a fuzzer and reported by Brandon (@serrebi).

SLPrefixMorph::AddAffix keeps a saved word's length in a signed byte and adds each affix to it unchecked. A run of the same letter is what makes it reachable — every position in the run offers the morphology the same prefix match — and past 127 the length reads back negative and memmove receives it unsigned. Brandon measured a single call asking for 4,294,956,106 bytes.

Brandon assumed Tiger reached it too, and said so, having no Tiger tree to check with. It does not. Rebuilt with the repair compiled out so it could not mask anything, all eight of the inputs that crash Vicki on Leopard render cleanly here. Same voice, same text, same host — only the SpeechDictionary differs.

The fix ships anyway: it is harmless (ordinary text renders byte-identically), and one behaviour to reason about beats two.

The part Tiger users actually gain

Brandon's PR #3 — three separate things that had to be wrong at once for a crash to arrive as pure silence:

  • A fault in the host's own code named nothing. The report could only resolve addresses inside Apple's images, so a fault in any CRT function the shim table hands the engine — strlen, memcpy — pointed nowhere. It now names the module and offset.
  • Everything identifying the caller was at debug level. The registers, the frame walk, the stack walk with C++ symbol names — all printed, all filed where nobody looks. A crash happens once and costs an utterance; it goes to warning now.
  • A reporter that faulted hung the host for ever. on_fault is a vectored handler, so it saw its own fault, found the flag already set, and slept forever — on the thread writing the report. The process never exited and the driver blocked on a pipe that would never answer.

That last one is strictly worse than the crash it was describing, and it was reproduced rather than reasoned about.

This is the machinery that made issue #4 findable at all. It is worth having before the next unexplained silence rather than after it.

Also since 0.7.7

Nothing else — 0.7.7's Tiger-image guidance (Intel only, and the earliest Intel Tiger, because 10.4.5 onward ships MacinTalk 3.4 with Apple's copy-protection call) still stands.

Testing

63 automated tests pass. Fred, Cellos and Vicki render byte-identically to the previous build.

tiger-speech 0.7.7 — which Tiger image works, and why 3.4 never will

Choose a tag to compare

@tgeczy tgeczy released this 19 Aug 23:11

If your Tiger disc is 10.4.5 or later, this add-on cannot run it, and that is now said out loud — while you are extracting, not after every voice turns out to be silent. That was issue #1, and the diagnosis went somewhere none of us expected.

Two things have to be true of your image

It has to be Intel. This loads i386 code and nothing else. A PowerPC Tiger disc is not a slower option, it is not an option: its MacinTalk is a thin big-endian PowerPC binary with no i386 slice in it. Intel discs carry a fat i386, ppc binary.

It has to be the earliest Intel Tiger — the 10.4.4-era disc that shipped with the first Intel Macs, carrying MacinTalk 3.3. From 10.4.5 Apple shipped MacinTalk 3.4, which calls ___commpage_dsmos — Apple's "Don't Steal Mac OS X" routine — from six places in the dictionary and three in the engine. On a real Mac a kernel extension answers that from the SMC. Off it, the call goes nowhere and the engine dies.

tools/extract_tiger.py now tests for both and names what it found.

Why 3.4 is a no, not a not-yet

Answering that call means shipping a fake SMC response whose only purpose is to defeat a copy-protection check. That is a different category from shimming mmap or printf, even though you supply your own licensed disc, and it is not a category this project is going into.

If your disc is 10.4.5 or later, use leopard-speech. Leopard's MacinTalk 3.6 dropped the check again, and it carries Fred and the whole MacinTalk 3 roster as well as Alex.

The part that turned out to be good news

3.4 is not 3.3 with a version bump. It is the turn toward Leopard, and it is measurable: 3.3 has no __cfstring section at all, so it has no tuning parameters whatever; 3.4 has thirty, from the same family Leopard grows to 283 — UnitCost.AccentCostWeight, SegmentAssembly.PitchSlack, the PitchDecrease/PitchIncrease group. Its code is smaller than 3.3's and it shares more symbols with 3.6, so it was rewritten rather than extended. The prosody machinery that makes Leopard's Fred sound like Leopard's Fred first appears in 3.4.

And PowerPC never got it. A real 10.4.11 PowerPC guest still reports MacinTalk 3.3 — PowerPC stayed on 3.3 for the whole of Tiger and went straight to 3.6 in Leopard. 3.4 was an Intel-only interlude of about eighteen months.

So 3.3 is not a lucky variant on an old disc; it is the Tiger Fred nearly everyone ever heard, and it is exactly what this add-on runs. Whether 3.4's Fred actually sounds different cannot be settled from here — the protection sits on the speak path, so it cannot be rendered at all. Anyone with period Intel hardware still running Tiger could answer it.

Also in this release

  • REG_STARTEND is honoured. The framework hands regexec a buffer that is not terminated, so abbreviations expanded only sometimes. Four wrong theories preceded the right one.
  • Every tuning parameter the engine asks for is named in the log, via TIGER_PREF_LOG.

Tiger's audio is unchanged: Fred, Cellos and Vicki render byte-identically, and the 55 automated tests pass.

tiger-speech 0.7.6 — a real regex engine, and Tiger unchanged

Choose a tag to compare

@tgeczy tgeczy released this 19 Aug 04:53

A shared-host release. Tiger sounds exactly as it did — that is the headline, and it was checked rather than assumed.

This add-on and leopard-speech run the same engine host, so they ship together. Nearly everything in 0.6.0 over there is Leopard's: Tiger's MacinTalk 3.3 does not have the tuning parameters that release is about — it has no __cfstring section at all — and Tiger's dictionary uses flat tables where Leopard's uses regular expressions.

So the honest summary of this release for Tiger users is: nothing changes, and here is the proof. Fred, Cellos and Vicki were rendered before and after across three texts, including one built from the constructs the new code touches. All nine renders are byte-identical, hash for hash. The 49 automated tests pass.

What is actually in it

  • A real regular-expression engine, contributed by Brandon (@serrebi) — a backtracking POSIX ERE matcher with a bounded step budget, replacing a reader that understood one shape of pattern. Tiger's dictionary compiles no patterns, so this is inert here and transformative on Leopard. It comes with tiger_host --regex-check, 115 cases.
  • The engine's tuning parameters can be answered. Leopard's engine asks for 283 named settings and was told nothing; it can now be told something. Tiger asks for none of them, so this cannot reach it — which is why the setting is keyed by name rather than applied globally.
  • docs/engine-tunables.md — all 283 names with their addresses, what the phrasing ones appear to govern, and tools/cfstrings.py to regenerate the list from an engine you extracted yourself.

Why ship it at all, then

Because the host binary is the same file. A driver and a host that disagree about the protocol is the one failure this project has decided to design out, so they move together and say so. If you are happy with 0.7.5, nothing here will change your day; upgrade at your leisure.

Getting the engine

Unchanged: nothing of Apple's ships here. You supply your own Tiger install, and the add-on reads the engine and voices from wherever you extracted them.

tiger-speech 0.7.5 — the volume that would not come back, and speech that starts sooner

Choose a tag to compare

@tgeczy tgeczy released this 19 Aug 02:04

The volume comes back when you turn it up, and speech starts sooner.

Released in step with leopard-speech 0.5.0: both add-ons share one engine host, and every fix below was found in one or the other and applied to both.

The volume bug, which was a silence bug

Take the volume to 0 with Home and back to 100 with End, and the synthesizer stayed silent for good. Only 99 brought it back, because 99 is the one value that still sent a command.

[[volm]] is state on the speech channel, and it outlives the utterance that set it. Sending nothing at 100 therefore never meant "full volume" — it meant "whatever was set last", and the one setting where that is fatal is the one that can be zero. The command is now sent when the setting is off its default, once more when it returns, and then not again.

Inflection had the identical bug: a voice flattened to 0 stayed flat when the slider came back to the middle.

Speaking sooner

The audio is streamed from the engine as it is produced rather than accumulated first, so the first sound of a paragraph leaves the renderer in 11 to 53 ms instead of most of a second — flat, whether the text is 24 characters or 1117:

 17 chars ->  1.19 s of audio in  2 chunks; first sound after 24 ms
135 chars ->  6.36 s of audio in  5 chunks; first sound after 14 ms
540 chars -> 25.61 s of audio in 13 chunks; first sound after 12 ms

Thirty times the text, and the wait before the first sound does not move.

It is an improvement rather than a cure. Part of the wait lives past the renderer, in the audio device, and that part is unchanged. Interrupting is where it shows most: the next utterance used to wait up to 2255 ms behind the one you abandoned.

The samples are byte-identical to what the old path produced, and there is a test that renders the same request both ways and compares. A change to when audio arrives must not change what arrives — Tiger's renders are still exact.

Interrupting

Cancelling used to stop the sound and leave the engine synthesising the rest of a sentence nobody would hear, with the next thing you asked for queued behind it. Cancel now reaches the engine, and the audio already handed to the sound card is bounded rather than seconds deep.

Two faults found alongside it: a cancel arriving between requests could silence the next utterance entirely, and a rate that failed to apply fell back to the engine's own 180 wpm — which reads exactly like lag rather than like a wrong rate. It says so in the log now.

Also

  • Rate boost is now in the settings ring, so it can be toggled without opening the dialog.
  • Debug logging reports what each utterance cost: how much audio, in how many pieces, how long until the first sound and until the last.
  • The driver and its engine ship together. If the executable is ever older than the driver, it says so and falls back rather than going quiet.

Getting the engine

Unchanged: nothing of Apple's ships here. You supply your own Tiger install, and the add-on reads the engine and voices from wherever you extracted them.

tiger-speech 0.7.4

Choose a tag to compare

@tgeczy tgeczy released this 18 Aug 16:00

Faster, more expressive, and a sentence no longer falls apart because someone typed a curly apostrophe.

Rate boost

The engine was never the limit — 400 wpm was a constant we picked. Measured, it honours whatever it is asked for and stays perfectly stable well past anything useful:

asked Alex delivers
400 wpm 429
800 wpm 853
1500 wpm 1598, without a stumble

So Voice settings → Rate boost takes the top of the slider from 400 to 1200 wpm.

It is a switch rather than a wider slider on purpose. Widening the slider would have made everyone's existing setting faster without asking — the same mistake as a volume control that arrives at half. It is off by default, and there is a test for that as well as for the boost reaching the engine rather than only the arithmetic.

Inflection

New slider, and it is the engine's own pmod — which turns out to be a percentage, not the 0-to-2 value its name suggests.

It reaches the two engine families quite differently. On the concatenative voices it opens the voice right up: Vicki's pitch wander runs from 7.1 to 31.2 across the range, Alex's from 8.6 to 22.5. On formant voices like Fred the wander barely moves (24.2 to 28.8) and the pitch itself climbs instead, 101.4 Hz to 125.9. Both directions are useful; neither is a bug.

Nothing at all is sent at the halfway point, because "no command" is not quite the same as pmod 100, and the default has to be the engine exactly as it comes.

A curly apostrophe is an apostrophe

Reported as the speech "breaking up" mid-sentence at points with no punctuation. Rendered as one utterance, with no splitting of any kind, the break was still there — so it was never about where the sequence was divided.

MacRoman has the typographic apostrophe, at 0xD5. But 0xD5 is the right single quotation mark, and the engine's front end treats it as one and breaks the phrase. Apple's 2007 front end never expected web text, where every apostrophe is curly. Measured on one reported sentence, apostrophes the only difference:

curly     7.68 s, with a 250 ms pause inside the word "Canopy's"
straight  6.11 s, and no such pause

Curly double quotes are deliberately left alone: those really are quotation marks, and the engine is right about them.

Pauses at line breaks

An index marks a position; it does not end an utterance. NVDA puts one at the start of every line during say-all, and it has already decided those lines belong together — sayAll speaks through speakWithoutPauses, which buffers lines until one contains a natural pause. Splitting there handed the engine a fragment ending in nothing, which it reads as a sentence ending. With word wrap on that is heard as a full stop in the middle of a sentence.

The audio stays whole now, and the indexes are still all reported.

Also

The driver logs the exact string it hands the engine when NVDA's debug logging is on. Three separate "it pauses oddly" reports have now had three different causes, and reconstructing the text from a truncated log is guesswork.

If the embedded-commands option seems to do nothing, that is NVDA rather than the add-on: symbols.dic lists [ and ] at punctuation level most, so at that level or higher NVDA rewrites the brackets into "left bracket" before the synthesizer sees them, and no command can survive. Set punctuation to some or lower and they work.

tiger-speech 0.7.3

Choose a tag to compare

@tgeczy tgeczy released this 18 Aug 12:10

Four people reported four things this morning. They turned out to be one bug each, and every one of them was in the driver rather than the engine.

The pauses before links

Kara noticed it first: "the speech pauses between reading text on a webpage and a link." Johann confirmed within a minute — "way too long on certain things."

NVDA hands a synthesizer a speech sequence, and I had assumed each string in it was its own utterance. It is not. Reading NVDA's own speech/manager.py, an IndexCommand is inserted only where a callback sits or an utterance genuinely ends. So a line of a web page with a link in it arrives as several plain strings with nothing between them at all — and tiger-speech rendered each one separately.

That means every fragment got the falling intonation and the final lengthening of a completed sentence. Not a gap in the audio — a gap in the prosody. Measured on one line, splitting it cost 163 ms across two joins, and there was no silence anywhere to trim. The extra was in the speech itself.

The pieces are joined now, up to the next index. This costs nothing in index accuracy, because there was never an index between them to lose — which is also why say-all is completely unaffected, and why no index-ahead trickery was needed.

There is also a Pause between phrases setting now, Short / Medium / Long, for people who want the opposite of what Kara wanted.

Alex saying "AI"

Jerry: "when he reads a story, and if the author uses the — character, alex calls that AI." And the strange part he spotted himself — reading the same text word by word said "em dash" correctly, only say-all got it wrong.

That detail is the whole diagnosis. In review mode NVDA substitutes the symbol's name before the text ever reaches the synthesizer; say-all sends the raw character. And the driver was sending UTF-8, where the engine's front end reads a single-byte Mac encoding — so one em dash arrived as three bytes and was read a character at a time.

Transcribed, before and after:

before   He paused, he eyed and left.  She said, ah hello, I'm stuck.
after    He paused, then left.  She said hello and stopped.

MacRoman already has the em dash, the en dash, the curly quotes and the ellipsis, so encoding it properly is the entire fix. There is no table of symbol names to maintain, and adding one would have been the wrong repair. Characters MacRoman genuinely lacks become a space rather than "?", because the engine reads a question mark as a question and lifts the intonation of the whole sentence.

This bug was in every release so far, and in every voice.

Capitals

"Capital pitch change percentage" is 30 by default and did nothing here, at any value. NVDA expresses it as a PitchCommand in the sequence, and the driver kept IndexCommand and threw away everything else it was handed. BreakCommand went in the same bin — so the one place NVDA explicitly asked for a pause was the one place it never got one.

Both are honoured now.

Volume

There was no volume control at all, which people quite reasonably named as a gap.

Rather than scale the samples afterwards, I checked whether the engine could do it, and it can: Apple's own [[volm]] command is exactly linear — 0.5 halves the RMS, 0.2 fifths it. So the synthesizer does the arithmetic in floating point before it quantises to 16 bits, which is better than anything that can be done to the samples after the fact. Nothing at all is added at full volume.

Nothing else moved

Vicki still renders byte-for-byte identically to a file made before any of this week's work — the whole point of a change that touches the driver is that the engine's output should not shift underneath it. The test suite went from 19 to 29.

One of those new tests reads the driver's own source to confirm the encoder is actually called, not merely present and correct. It caught exactly that mistake in the sibling add-on within the hour. An encoder nobody calls is precisely as broken as no encoder.


Re-uploaded shortly after publishing

The first 0.7.3 asset made everyone slightly quieter, and Jerry caught it within minutes: "alex got quieter, not by a whole lot, but it was definitely noticeable."

Adding a volume control is what did it. NVDA's NumericDriverSetting takes defaultVal=50, that becomes the config spec's default, and NVDA writes it over whatever the driver set for itself. So the setting arrived at half.

It defaults to full now, which is the only defensible answer for a synthesizer that had no volume control at all the day before: upgrading should change nothing until you ask it to.

If you installed 0.7.3 in its first hour, NVDA will not offer you this as an update, because the version number has not changed. Either raise the volume slider once, or reinstall the add-on from this page.

tiger-speech 0.7.2

Choose a tag to compare

@tgeczy tgeczy released this 18 Aug 05:51

The engine folder reminder has never worked. Not once, in any release. Here is why.

Brandon guessed the tree "probably needs some healing recovery in it or something." The tree did, in fact, need healing.

One line, two evenings

When tiger-speech cannot find the engine, a dialog is supposed to appear at start-up telling you where to put it. Several people reported no synthesizer in the list and no dialog — and those two look identical from outside, because both are just nothing happening.

I blamed the start-up timing. I blamed a suppressed reminder. I read NVDA's source to see whether wx existed yet when add-ons load. All wrong. A user sent a log:

AttributeError: module 'wx' has no attribute 'YES_NO_CANCEL'

wxYES_NO_CANCEL is real in wxWidgets' C++ API. wxPython does not have it. The dialog asked for it anyway, so it threw every single time it ran, in every release, on every machine — and the exception went into a log nobody had a reason to read.

Fixed. It is wx.YES_NO | wx.CANCEL, and the dialog now appears.

So it cannot happen again

A test reads the add-on's own source and checks every wx.NAME against the set wxPython actually has. A misspelt constant now fails on my machine instead of silently on yours.

The first version of that test matched nothing at all and passed vacuously — an escaping slip had turned part of its pattern into a literal control byte. It now proves it found files, and found names, before it trusts what it did not find. A test that cannot fail is worse than no test.

Config paths

Raised by a user, and worth stating: the engine folder is resolved from NVDA's config directory, never by expanding %APPDATA%. That is what makes it correct for a portable copy, or one started with -c. The add-on already did this; it is now hardened so an empty answer is rejected rather than passed along.

Also

Two host changes, both Leopard-only in effect and both regression-checked against Tiger — Fred and Cellos still render byte-for-byte identically. Leopard's engine now loads its own C++ runtime and gets much further; it is not finished, and it is not in this add-on.

Nothing of Apple's ships here; you supply the engine from your own Mac OS X 10.4 install.

tiger-speech 0.7.1

Choose a tag to compare

@tgeczy tgeczy released this 18 Aug 05:32

The synthesizer is now always in NVDA's list, and tells you what is wrong if it cannot run.

Why

Until now tiger-speech hid itself when it could not find the engine. The idea was sound — a synthesizer that can be chosen and is then silent leaves you with no speech and no way to hear your way back out.

But that describes a driver which loads and then makes no sound. It does not describe one that refuses to load. NVDA catches that, falls back to the synthesizer you were using, and speech never stops.

Hiding cost the explanation. The start-up dialog was meant to cover it, and on at least one machine it never appears at all — leaving no dialog, no synthesizer, and nothing to tell that apart from the add-on not being installed. One dialog fired from a timer during start-up is a fragile place to keep the only answer.

What changes

tiger-speech is always listed. If the engine is missing, choosing it fails cleanly and shows a dialog naming the exact folder the engine belongs in, with a button that opens it. Every time, with no dependence on catching you during start-up.

You will see NVDA's own "Could not load the tigerspeech synthesizer" message as well. That is expected — ours appears on top of it and is the one with something to act on. Your speech is not interrupted: NVDA falls back to the synthesizer you were already using.

The start-up dialog and the log diagnostics from 0.7.0 are unchanged, and the NVDA log still holds the full list of what was found and what was missing.

Also

If you once answered "No" to the start-up dialog, that is remembered in a do-not-ask file in your tigerspeech-data folder — delete it to be asked again. Selecting the synthesizer works regardless of that file.

Nothing of Apple's ships here; you supply the engine from your own Mac OS X 10.4 install.

tiger-speech 0.7.0

Choose a tag to compare

@tgeczy tgeczy released this 18 Aug 05:11

If tiger-speech does not appear in NVDA's synthesizer list, install this and check your log. It will now say exactly why.

Why the synthesizer might be missing

Several people extracted the engine correctly, saw the three expected folders in tigerspeech-data, and still had no synthesizer in NVDA's list — with nothing anywhere to explain it. NVDA only writes:

Synthesizer 'tigerspeech' doesn't pass the check, excluding from list

The add-on's own check had four separate ways to fail — no host executable, no tree found, missing engine binaries, no readable voices — and all four looked the same from outside.

It now reports which one. On an affected machine the NVDA log will contain a line like:

WARNING - tiger-speech is not available:
  host: ...\tiger_host.exe found
  data folder: ...\tigerspeech-data exists
    contains: SPSupport.framework, Speech, SpeechDictionary.framework
  tree: ...\tigerspeech-data
  MacinTalk: ...\MacinTalk found
  SpeechDictionary: ...\SpeechDictionary MISSING
  voices: 0 playable of 23 present in ...\Speech\Voices

If it is still missing after installing this, please send that block. It is enough to identify the problem without any further guesswork.

The start-up dialog is instrumented the same way. "No synthesizer" and "no dialog either" previously looked identical — like nothing happening at all — and the log now distinguishes the plugin never running, the check passing, and a do-not-ask marker suppressing the dialog. If you once answered "No" to that dialog, delete the do-not-ask file in your tigerspeech-data folder to be asked again.

Also in this release

A file-identity fix in the host: stat reported no st_dev or st_ino, so anything keying on file identity saw every file as the same file. Tiger never exercises that path, and renders are byte-identical with and without it, but it is a real correctness fix and it is what allowed Leopard's engine to open a speech channel for the first time.

No change to speech output. Nothing of Apple's ships here; you supply the engine from your own Mac OS X 10.4 install.

0.6.0 — Tiger speaks, all twenty-three voices

Choose a tag to compare

@tgeczy tgeczy released this 18 Aug 04:11

The first public release, and the first that has been through real users on real machines.

Apple's engine runs as native code. No virtual machine, no emulator, no CPU translation — a 32-bit host process maps Tiger's Intel MacinTalk into memory and calls SESpeakBuffer directly. An utterance costs about twelve milliseconds.

What you get

All twenty-three voices from Mac OS X 10.4, across the three engines in that bundle:

  • MacinTalk 3 — Fred as he sounded in 2005, plus Kathy, Princess, Junior, Ralph, Whisper, Zarvox, Trinoids and the novelty and singing voices
  • MacinTalk Pro — Bruce, Victoria, Agnes
  • meow — Vicki, who has an engine to herself because her sample bank is AAC

Rate spans 80–400 wpm. Pitch is measured in semitones from each voice's own natural pitch, so the middle of the slider is the voice as Apple recorded it. Embedded speech commands ([[slnc 500]], [[volm 0.5]], even [[inpt TUNE]] for singing) all work, and are off by default so a web page containing two square brackets cannot change how your screen reader sounds.

No Apple software is distributed here

package.py refuses to build an add-on containing any. You supply your own Tiger install, and tools/extract_tiger.py pulls the engine out of your own disc image.

Put the extracted tree in %APPDATA%\nvda\tigerspeech-data\not in the add-on folder, because updating an add-on deletes and recreates its directory.

Fixed in 0.6.0, both found by users

  • Windows 7's AAC decoder returns one frame fewer than Windows 10 and 11 do, which put Vicki's audio 1024 samples out of place and ran her syllables together. The host now flushes each decoder's own latency rather than inferring it, so the version of Windows no longer matters.
  • The singing voices went permanently silent on long text. They render several times more audio per character than the rest, so an ordinary long message tripped a guard meant to catch a stalled pipeline — and tripping it stopped the engine's clock. The same bug had been silently truncating long messages with those voices for as long as they have worked; Good News now renders a post in full where it used to stop halfway.

Requirements

NVDA 2023.1 or later, 32-bit or 64-bit. Vicki additionally needs an AAC decoder, which every edition of Windows ships except N and KN until the Media Feature Pack is installed; where it is absent she is left out of the voice list rather than offered and silent.