Skip to content

Latest commit

 

History

History
762 lines (649 loc) · 42.4 KB

File metadata and controls

762 lines (649 loc) · 42.4 KB

Speech Output Engine

A desktop app that reads your documents out loud, and saves the result as an audio file. It is built around being usable by people who cannot see the screen, cannot use a mouse, or find small low-contrast text hard going.

Choose a file, choose a voice, choose what to do with it, press Apply. That is the whole app. Speech comes from the voices already built into macOS and Windows, so it works with no account and no setup; add an ElevenLabs API key and you get their voices instead. It reads text, Word documents, PDFs, tables and PowerPoint decks. Images work too: hand it a photo or a screenshot and a vision model running on your own machine reads the text out of it — and so does video, which is taken apart into stills and described a frame at a time.

The window is two panes. The list on the left — Read a File, Audio Player, Dictionary, Settings, Shortcuts — chooses what the right-hand pane shows.

Accessibility

This is the point of the app rather than a feature of it.

  • Everything works from the keyboard. Every control is reachable with Tab, every action has a shortcut, and the whole list is in the app under Shortcuts (or press F1). Nothing is mouse-only.
  • Every control is labelled, and the label is tied to its control, so a screen reader announces "Speech engine, combo box, System voices" rather than reading a value with no name. Buttons say what they do in words.
  • The focused control is obvious — a heavy accent-coloured outline, not a faint one-pixel ring.
  • Colour is never the only signal. Status messages are prefixed with "Done:" or "Problem:" as well as coloured, and — unless you turn it off — starting, finishing and failing each make their own sound.
  • The status line speaks for itself. It is a live region, so a screen reader reads out how an action ended as it happens, rather than waiting for you to go looking for it. A failure interrupts; anything else waits its turn. The running commentary while a long job works is deliberately left out of that — it changes several times a second, and the progress tick is its channel.
  • Contrast is designed, not inherited. Every text colour in src/theme.rs is written down as a pair with the surface it sits on, and clears 4.5:1 in both the light and dark themes — most clear 7:1.
  • Light or dark, your choice. Under Settings → Appearance: follow the computer, or pin the app to light or dark whatever the rest of the machine is doing. It changes as you pick it, with no restart. A dark window on a bright desktop is an ordinary thing to want if screens are hard on your eyes.
  • One column, one width. Every control in the form is the same width and the layout never reflows, so a screen magnifier parked on the left edge stays useful all the way down.
  • A real bold face is bundled (Ubuntu Bold). egui ships only a light weight, and at these sizes weight is the biggest single readability difference.

What it does

  • Reads .txt and .docx. Word documents are unpacked and only the readable text is kept — no style names, no stray tab stops, and character references like & and é come through as & and é.
  • Reads .pdf. The page is redrawn and the words are worked back out of where the glyphs land, since a PDF stores neither paragraphs nor words — see Reading PDFs.
  • Reads .pptx. Each slide is announced by number and title before anything on it, so you always know where you are — see Reading presentations.
  • Two speech engines. The voices built into your operating system, or ElevenLabs. Choosing ElevenLabs asks for an API key there and then, with a link to where ElevenLabs keeps them, and checks the key as soon as you paste it. A key ElevenLabs turns down is removed rather than kept.
  • Reads aloud, or saves to WAV or MP3. One dropdown, not two buttons.
  • Plays audio files back. The Audio player pane takes any WAV or MP3 — one this app saved, or an audiobook chapter from anywhere else — with play, pause, stop, a ten-second rewind for the sentence you missed, and a countdown of how much is left. Drop a file anywhere on the window and it lands there.
  • Plays a zip of them as a playlist, one track after another, with music able to come up underneath the speech it follows — see Playlists.
  • A sound when something starts, finishes or fails. A short tone as the work begins, a chime for success, a lower tone for a problem, so you know what happened without watching the status line. While the work runs there is a quiet tick every fifteen seconds — a long job and a stuck one sound different — which has its own switch, since it is the one sound that reports nothing new. Neither ever plays over a document being read aloud, and the audio player stays quiet throughout, since there the sound is the point. All of it is under Settings.
  • A dictionary of word replacements — see below.
  • Reads images (.jpg, .png, and .heic/.heif on macOS) through a local Ollama vision model. Nothing is uploaded anywhere.
  • Describes video (.mp4, .mov, .m4v, .avi, .mkv, .webm). Stills are taken wherever the picture changes enough to be a new shot, plus one every half minute so a long unbroken take is not described by its opening frame alone. Each still goes to the same vision model, and the answers are then written up as one continuous description. Nothing is uploaded anywhere. From there it is an ordinary piece of text: read it aloud, or save it as WAV or MP3 like anything else.
  • Summarises network captures (.pcap, .pcapng, .cap) — see Reading network captures. Every packet is counted here on your machine; a local text model then writes those figures up as something worth listening to, and if it cannot, the counted figures are read out as they are.
  • Offers to install Ollama and ffmpeg — with Homebrew on macOS, winget on Windows — the first time you open a file that needs one. It never installs anything without asking, and it shows you the command it would run.
  • A right-click "Speak to file" entry, on Windows. Turn it on once in Settings and any text, Word, PDF, CSV or PowerPoint file gets a Speak to file option in Explorer's right-click menu — no window opens, the audio just appears next to the file, using whatever engine and voice you last used.

The dictionary

Words listed under Dictionary are swapped just before the document is spoken. Two uses:

  • Pronunciation. A synthesiser that says "Siobhan" wrongly will say "Shivawn" correctly, and you need that fix in every document, not once.
  • Substitution. Swap a word for a gentler one so a document can be read out in a room with children in it.

Matching ignores capitals, and a word that started a sentence still starts one after the swap, so a single rule covers "shit" and "Shit". Whole word is on by default, so a rule for "cat" leaves "catalogue" alone. Rules apply in order, and the file on disk is never touched — the replacement happens on the way to the voice.

Playlists

Choose a .zip in the Audio player instead of a single file and every WAV and MP3 inside it becomes a track, played one after another. The pane says which track is playing and how many there are, the status line says so again each time one starts — that being the line a screen reader is already watching — and play, pause, stop and the ten-second rewind all work within the track that is playing.

Nothing is ever unpacked. The archive is opened and each track is decompressed in memory as its turn comes, so a playlist leaves nothing behind on disk.

Without a running order the tracks play in the order their names read, so track2.mp3 comes before track10.mp3 rather than after it. To set the order yourself, put a media.txt in the zip:

<music>
    <audio type="music" pos="1">intro.mp3</audio>
    <audio type="speech" pos="2">briefing.mp3</audio>
    <audio type="music" pos="3">words.mp3</audio>
</music>
  • pos is the running order. Leave it out and the order is the order the lines are written in.
  • type is speech or music, and it is what makes the join below. A track with no type is speech.
  • Names are matched without regard to capitals or which folder inside the zip the file sits in, so Intro.MP3 finds audio/intro.mp3.
  • A file the running order never mentions is still played, after the ones that are, rather than silently left out. A file the running order asks for that is not in the zip is noted in the log (⌘L).

The file is called .txt for a reason: a plain list of file names, one per line with # for comments, works just as well as the XML above. Everything in a plain list is speech.

Music under speech

When a music track follows a speech track, it does not wait for it. The music starts 1.25 seconds before the speech ends and fades in across exactly that time, so it is at full volume as the last word lands — the join a radio bulletin makes between the newsreader and the outro, rather than a gap and then a jolt. Where the speech track cannot report its own length, the music still fades in; it just starts where the speech stops. Rewinding takes the music back off again until the speech reaches its end once more.

Keyboard shortcuts

on macOS, Ctrl on Windows.

Keys What it does
⌘O Choose a file — a document, or an audio file or playlist in the player
⌘Return Apply — run the chosen action
⌘. or Esc Stop reading or playing, or cancel what is running
⌘1⌘5 Go to Read, Audio player, Dictionary, Settings or Shortcuts
⌘P Audio player: play, or pause if already playing
⌘R Audio player: skip back ten seconds
Up Down Move along the list of panes, once it has focus
Tab / Shift+Tab Move between controls
Space or Return Operate the focused control
Left Right Change the value in an open dropdown or a slider
⌘K ElevenLabs API key
⌘L Show or hide the activity log
F1 The Shortcuts pane

Other languages

The interface can be translated by editing one plain text file. No programming, no build, no toolchain — and a half-finished translation still leaves a working app, because every line you have not written yet falls back to English.

English and a French draft are built in. Settings → Language picks between them, or follows the operating system.

Writing a translation

  1. Open Settings → Language and press Open Language Folder.
  2. Copy assets/lang/en.toml into it, named for your language — de.toml, pt-BR.toml.
  3. Change the three lines at the top: code, name (written in your own language, so it reads "Deutsch" rather than "German"), and plural.
  4. Translate the right-hand side of each line. Leave the keys on the left alone.
  5. Press Reload Language Files and watch the app change. No restart.

A file whose code matches a built-in language replaces it, so a shipped translation can be improved as well as a new one added. Anything the app could not read is reported by line number under the Language setting rather than silently dropped.

The format is a comment, a key, and a quoted value:

code   = "de"
name   = "Deutsch"
plural = "one_other"

# Choose a file, a voice, and what to do with it
pane.read.hint = "Wählen Sie eine Datei, eine Stimme und was damit geschehen soll"

# Reading aloud… {n} words replaced by the dictionary.
status.reading.one   = "Wird vorgelesen… {n} Wort ersetzt."
status.reading.other = "Wird vorgelesen… {n} Wörter ersetzt."

Every entry in en.toml carries its English source in a comment above it, so the reference is never in another window. Three things to know:

  • {C} becomes on macOS and Ctrl on Windows. Put it wherever your sentence needs it.
  • {name} and friends are values the app fills in. Move them anywhere in the sentence — that is what they are named for — but do not rename them.
  • .one / .other are counted messages. plural at the top of the file says which forms yours needs: one_other, french (zero takes the singular), polish, or russian.

The prompts sent to the vision model are in the file too, and they are worth translating: a German prompt gets a German description that a German voice can read. A prompt you have edited yourself in Settings is never overwritten by a language change.

What is not translated yet

  • Error text from the reading enginessrc/extract/, src/tts/, src/ffmpeg.rs, src/ollama.rs — is still English. Everything on screen in normal use is not.
  • Arabic, Hebrew and other right-to-left languages cannot be laid out correctly: egui has no bidirectional text support yet, so they would render backwards. This is not something a language file can work around.
  • Chinese, Japanese, Korean and Vietnamese need a font the app does not bundle. A test refuses any language file containing characters the bundled fonts cannot draw, so this fails loudly at build time rather than quietly as rows of ? on somebody's screen. Latin, Greek and Cyrillic are all covered.

Requirements

macOS or Windows. The speech engines are say on macOS and System.Speech (SAPI 5) through PowerShell on Windows; both ship with the operating system. HEIC photos are converted with sips, which is macOS-only — on Windows, save them as JPEG or PNG first. Video needs ffmpeg; the app offers to install it when you open one.

You need Rust 1.88 or newer to build. There are no C toolchain surprises on Windows: the TLS stack is ring rather than the default aws-lc, which would want CMake and an assembler.

Linux is not a supported platform, but it is not shut out either. Every platform-specific corner of the code already has an "everywhere else" branch, so the source is written to compile there — though no release is built or tested on Linux, and that branch is where the system speech engine is missing rather than merely different. You would need an ElevenLabs key to get any speech at all. If you want a voice that comes with the machine — espeak-ng, or Piper for something actually worth listening to — open an issue. Nobody has asked yet, and that is the only reason it is not there.

Building and running

cargo run --release

You can also pass a file to open on startup, which is what "Open With" does on both platforms:

cargo run --release -- ~/Documents/report.docx

To run the tests:

cargo test

The suite covers the .docx parser, the PDF reader — its object syntax, stream filters, page tree, font encodings and the page-description interpreter, each tested separately — the dictionary matcher, text chunking, WAV/MP3 encoding, the words-per-minute to SAPI rate conversion, and — on macOS and Windows — renders a real sentence through the system voice and checks the audio that comes back is neither empty nor silent.

Using ElevenLabs

Without a key the app just works, using the system voices. To use ElevenLabs, choose it in the Speech engine dropdown and the key dialog appears; you can also reach it any time with ⌘K. The key is saved in elevenlabs.key in the app's own settings folder — under ~/Library/Application Support on macOS, %APPDATA% on Windows — as plain text, in a file only your account can read. It never goes on a command line where another process could read it.

Earlier versions kept it in the macOS login keychain and in a DPAPI blob on Windows. Both reached their storage by handing the key to another program, and both had ways to fail that produced a wrong answer without saying so; a key saved by one of those versions is moved into the file above the first time this one runs. See Security for what the change does and doesn't cost.

If you'd rather not store it at all, set the environment variable instead — it takes priority over the stored key and nothing is written to disk:

export ELEVENLABS_API_KEY=sk_…

There is no length limit. ElevenLabs caps a single request at 10,000 characters, so anything longer is split into requests of at most 4,500 — at sentence boundaries, never mid-word — and the returned audio is joined back into one continuous recording. This applies equally to reading aloud and to saving, so a book-length document plays and saves as one file; while it runs, the status line says which part it is on. Each request carries the neighbouring text as context so the voice doesn't reset its intonation at the seams. Audio is cached per (text, voice, model), so listening to something and then saving it doesn't pay for the same synthesis twice.

Reading PDFs

A PDF is not a document in the sense the other readers here deal with. A .docx says "this is a paragraph, and these are its words". A PDF says "put this glyph at this point on the page" — everything a reader needs, including where a word ends and where a line breaks, was thrown away when the file was made. So reading one is reconstruction: the page is redrawn, and the words are worked back out of where the glyphs land.

Two details do most of the work. Character widths are read from the file's own embedded fonts, which is what tells a real word gap from ordinary letter spacing — without them a line positioned glyph by glyph comes out as "M a c B o o k". And the file is scanned for its objects rather than seeking to them through its index, because a PDF that has been edited almost always has a stale index, and trusting it reports readable documents as damaged.

Nothing is uploaded, and nothing needs installing. Unlike images and video, a PDF is read entirely inside the app, in a second or two, which is why it is also offered in the Windows right-click menu.

When a PDF cannot be read

Three cases give up no text, and the message says which one you have, because they need quite different things done about them:

  • A scan. A PDF from a scanner or a phone is a photograph of a page with no text in it at all. Nothing can parse words out of one — but the image reader can, so save the page as a JPEG or PNG and open that instead.
  • Fonts that number their glyphs instead of naming them. The file shows text, but records nothing about what its letters are. This is what a Chinese, Japanese or Korean document typeset before /ToUnicode became usual looks like, and reading it needs character tables that ship with a PDF viewer rather than with the file. Opening it in a viewer and copying the text out works.
  • Encryption. Including the very common kind with no password on it, only a restriction on printing or copying — the text is still scrambled. Saving a fresh copy from a PDF viewer usually removes it.

A code no font can account for is dropped rather than guessed at. A voice reading confident nonsense is worse than a quiet one.

Pages that only partly decode

The awkward case is a document that is mostly fine. A long English report with a section in Chinese, Japanese or Korean opens, reads correctly for a hundred pages, and then reaches pages whose fonts do not say what their letters are. Those pages come back with a third or more of their characters missing — which looks like ordinary text on screen and is nonsense when spoken.

This is judged per page, not across the file, and that distinction is the whole point: measured over a long document those pages are a rounding error, so a whole-file check stays silent exactly when it is needed. Per page the two are nowhere near each other — a page whose fonts decode loses a glyph or two in several thousand, and a page whose fonts do not loses a third of itself or more.

When it happens the status line says how many pages were affected, and the log names them and says what to do. The text is still there and still read out; nothing is hidden and nothing is dropped on your behalf.

Where such a page gave back nothing at all, a line stands in its place:

[Pages 27 to 41 could not be read: the fonts used there do not say what their letters are.]

Otherwise those pages would simply be absent, and a gap in a spoken document is indistinguishable from the document having nothing more to say — you would never know a section had been there. One marker covers a whole run of consecutive pages: on the file this was built against that is a fifteen-page stretch, which announced page by page would interrupt fifteen times to say the same sentence.

Reading presentations

A slide is a heap of floating text boxes. There is no reading order in the file worth the name, and nothing in it says which box is the point — so a deck read out as one long run of text is a stream of fragments with no way to tell where one slide stopped and the next began.

So the shape is put back before anything is spoken:

  • Every slide is announced by number and title in one breath — "Slide 4 of 12. Second-quarter results." A listener who has lost their place gets both at once, rather than a bare number or nothing at all.
  • Bullet points get a full stop each. Slides are written without punctuation far more often than not, and three bullets run together are one baffling sentence.
  • Speaker notes are read, under a "Speaker notes" heading at the end of the slide. They are very often the actual sentences the three bullet points above them were an aide-memoire for, and they live in a separate part of the file that nothing reading only the slides will ever find.
  • Pictures are announced by their alt text — the one part of an image written for somebody who cannot see it. A picture that has none is still announced as "Image with no description", because a picture nobody described is a hole in the slide, and silence there is indistinguishable from a slide with no picture on it. The automatic name PowerPoint gives a shape ("Picture 3") is never read: it sounds like a description and is not one.
  • The slide number, footer and date placeholders are left out. They are the same words on every slide of most decks, and hearing a footer forty times buries the forty slides it was on.
  • Hidden slides are skipped, and do not take up a number. They were hidden deliberately, and a deck read aloud should match the deck as presented.

The running order comes from the deck's own list of slides rather than from the names of the files inside it. Moving slide 12 to the front in PowerPoint rewrites that list and leaves slide12.xml called slide12.xml, so sorting by filename would present a reordered deck in the order it was first written.

Only .pptx is read. The older .ppt is a 1997 binary format that shares nothing with it but a syllable; opening one gets a message telling you to open it in PowerPoint and save it again as .pptx, rather than a shrug.

Reading images

The first time you open an image, the app checks for Ollama and offers to set up whatever is missing:

  1. If Ollama isn't installed, it offers to install it — brew install ollama on macOS, winget install Ollama.Ollama on Windows — and shows you the command first. If neither package manager is there, it links to the downloads instead of guessing.
  2. If the vision model isn't downloaded, it offers to pull it and shows the progress.
  3. Then it starts the server if needed and reads the image.

The default model is qwen2.5vl:3b — about 3 GB, and good at reading text. Settings offers a handful of alternatives with their download sizes, and will take the name of any other Ollama vision model you type in. The prompt is editable there too; the default asks for a verbatim transcription of any text in the image, falling back to a short description if there isn't any.

Ollama occasionally retires the runner an older model was built for, at which point that model stops loading no matter how many times it is downloaded — llama3.2-vision, which earlier versions of this app defaulted to, went that way. Settings will not let you keep a model in that state: the saved name is swapped for a working one on upgrade, and the error you get names the model and points at the dropdown rather than repeating Ollama's own wording.

Small models sometimes answer an elaborate prompt with nothing at all. If that happens the app retries once with a plain question rather than reporting failure, and says so in the log — which is what makes a 1.7B model like moondream usable here as well as the larger ones.

A JPEG or HEIC photo's description ends with a line saying it was written by a local AI model running on Ollama, not transcribed from the photo — the same disclosure video gets, and for the same reason: what is heard is a guess at the picture, worth knowing wherever the description ends up. That line also says which half of the pipeline just left this computer: read with a system voice, it says so; read with ElevenLabs, it says the text is being sent to that cloud service to be spoken, so saying only "a local AI model" isn't the whole story. On by default; turn it off under Settings. A PNG gets no such line, since one read here is far more often a screenshot or a diagram than a photo.

Describing video

A video is read the same way as an image, several dozen times over. ffmpeg takes stills out of it, each still goes to the vision model, and a model then rewrites the answers as one continuous description. The first time you open a video the app offers to install ffmpeg if it is missing, the same way it offers Ollama.

The cost is per frame, not per video. On a machine with no graphics card a single frame can take the better part of a minute, so which frames get taken is the setting that matters. Three controls under Settings decide it:

  • How much of the picture must change for a new frame. ffmpeg scores how different each frame is from the one before it; anything at or above this counts as a new shot. Low takes a frame when the camera merely moves, which describes more and takes longer. High takes one only at a clear cut.
  • Take a frame anyway after this long. A slow pan across a landscape never trips a cut, and without this it would be described by its opening frame alone. Thirty seconds by default.
  • Most frames to describe from one video. The stop that keeps a long or busy video from taking the rest of the day. Forty by default, two hundred at most.

So a ten-minute lecture on one slide costs a handful of frames, and a fast-cut trailer of the same length costs many — which is the right way round.

The write-up is done by the vision model itself unless you name a text model in Settings, so nothing further has to be downloaded to use this at all. A dedicated text model usually writes better prose; a 3B vision model doing both jobs will sometimes lose a frame or two out of the middle. If the write-up fails or comes back as a stub, you get the frame-by-frame account instead — each description under the time it appears — rather than an error, and the log says which you got. That account is also what you get with the write-up turned off, and it is the more trustworthy of the two: every sentence in it came from a frame, where a joined-up narration is a model's account of what connects them.

A video's description ends with a line saying it was written by a local AI model running on Ollama, not transcribed from the video — a fact worth knowing wherever the description ends up, not only in the one-time warning shown before it starts. That line also names which half of the pipeline just left this computer: read with a system voice, it says so; read with ElevenLabs, it says the text is being sent to that cloud service to be spoken. On by default; turn it off under Settings. JPEG and HEIC photos get the same line, for the same reason — see Reading images above.

Reading network captures

A .pcap is a list of packets, and a list of packets read out is unusable — a quiet minute on one laptop is tens of thousands of lines, none of which means anything on its own. What anyone actually wants to know is the shape of it: who talked to whom, what about, for how long, and what went wrong.

So a capture is read in two passes, and the split between them is the whole design:

  1. Everything is counted, here, on your machine. Packets are decoded as far as their addresses, ports, protocol and TCP flags; conversations are gathered so that a reply lands on the same entry as the request; the domain names asked for over DNS are collected, since those are what actually say what a machine was doing. The result is an ordered account of the facts, and every number in it was counted rather than inferred.
  2. A local text model turns those facts into English. It is given the counted account and asked only to write it up — not to decide what the facts are. If Ollama cannot be reached, or the model answers with a stub, the counted account is read out as it stands, so a failure costs you some fluency and none of the figures.

That division is deliberate. A model handed a table of addresses and ports knows a great deal about what such traffic usually means and will happily supply an explanation — an infection, a backup, a misconfigured server — that the packets do not support. A capture is nearly always being read because something has already gone wrong, so a plausible guess presented as a finding is the most expensive mistake this app could make. The prompt says so at length, and you can read and change it under Settings; the checkbox beside it turns the model off altogether and reads the counted figures instead.

A capture is read on your own computer unless you say otherwise. A summary names the machines on your network and the addresses they looked up, which together say a good deal about who was doing what — so unlike everything else this app opens, it is not handed to ElevenLabs just because the engine dropdown was set that way when you opened the file. With Allow captures to be read by ElevenLabs off, which is how it ships, a capture is read by a voice on this computer whatever that dropdown says, and the Read pane tells you so rather than quietly swapping the voice. It is the same answer the app gives to looking up where a photo was taken, for the same reason.

The summary ends with a line saying which parts were counted and which were written by a model, and which voice is about to read it — on by default, and worth keeping, because a sentence about what a network did tends to get repeated to somebody else as fact.

Both container formats are read — the classic .pcap in either byte order and either timestamp precision, and .pcapng — along with Ethernet, raw IP, BSD loopback and both Linux "any" pseudo-headers, IPv4 and IPv6, TCP, UDP, ICMP and ARP. VLAN tags are stepped over rather than allowed to hide the packet behind them.

Right-click "Speak to file" (Windows)

Turning this on in Settings adds a Speak to file entry to the right-click menu in Explorer for text, Word, PDF and CSV files — the file kinds that read in a second or two, with no separate setup. Choosing it reads the file, speaks it with whatever engine, voice and rate are currently saved, and writes the audio next to the source (as WAV or MP3, whichever Settings has you saving as) — no window opens. Running it twice on the same file never overwrites the first result; the second is numbered, like Windows does for a duplicate download.

Images and video stay in the app itself: they go through Ollama and ffmpeg, can take anywhere from seconds to the better part of an hour, and the app would have nothing to show for that time with no window open — no progress bar, no way to cancel.

The app ships as a single portable .exe, usually run straight out of Downloads and often deleted afterwards. So turning this on also copies the app into its own settings folder and points the right-click entry at that copy, not wherever it happened to be launched from — deleting the original afterwards doesn't break it. Everything here is written to HKEY_CURRENT_USER, so no admin prompt appears, and nothing outside your own Windows account is touched. Settings shows exactly where the copy lives, and turning the entry back off removes the right-click menu changes (the copy itself is left in place).

Security

The app runs other programs and opens files it did not write, so both are treated as untrusted.

  • Nothing you type or open ever reaches a shell. No filename, document, voice name or key is passed through sh or cmd, so there is no layer that could reinterpret a character in any of them; every program is launched with its arguments as a list. There is exactly one shell in the app — installing Homebrew runs bash, because Homebrew's installer is a shell script — and nothing of yours is in it. See the Homebrew note below.
  • Programs are named by absolute path/usr/bin/say, /usr/bin/security, System32\WindowsPowerShell\v1.0\powershell.exe. A bare name would let PATH, or the directory the app was unzipped into, decide which binary runs — and one of them is handed your documents to speak, another the old stored key to hand back.
  • The two programs that cannot be named that way are told where they may not come from. Ollama and ffmpeg are installed by somebody else, so they have to be looked up — and on Windows where.exe searches the current directory before PATH, which for an app run out of Downloads is the folder the app itself is sitting in. An answer out of that folder, or out of the working directory, is refused, and the rest of the answers are still considered: a copy planted in front of a real installation costs the planted one, not the ffmpeg you installed.
  • Documents never go on a command line. macOS pipes the text to say over stdin; Windows stages it in a file the PowerShell script deletes as soon as it has read it. Anything that is interpolated into a generated script — paths, voice names — is escaped for a PowerShell single-quoted string, and that escaping is unit-tested on every platform.
  • The API key never appears on a command line, so it is never in the process list, and it is kept out of the config file so that the file you would paste into a bug report holds no secret. It is stored as plain text in elevenlabs.key in the app's settings folder, created 0600 on macOS so no other account can read it. This is a deliberate step back from the keychain and DPAPI that earlier versions used: both worked by handing the key to another program, and both could fail in ways that stored the wrong thing and reported success. What they actually bought was less than it looks — the DPAPI blob decrypts for this same user, and the keychain item was written so that security could read it back without a prompt, so both were one subprocess away for anything already running as you. Reliably having the key the user typed was worth more than that margin.
  • Installing Homebrew downloads a script and runs it — with your consent, and only ever that one. It is Homebrew's own installer, from raw.githubusercontent.com over HTTPS, and it is what brew.sh itself tells you to run; the app shows you the command before anything happens, checks that what came back is a shell script rather than a captive portal's login page, and asks for your password with a macOS dialog that hands it to sudo and to nothing else. Two things are worth stating plainly: the script is fetched from a moving HEAD, so it is whatever Homebrew publishes at that moment, and nothing verifies its contents beyond the transport. That is the same trust you extend by running the command from Homebrew's own front page — but it is the one place this app runs code it did not ship, and you should know that before you press the button.
  • A playlist is read, never unpacked. A zip opened in the audio player has its tracks decompressed into memory one at a time. No entry name from the archive is ever used as a path to write to, entries whose names would climb out of the archive are left out of the running order rather than trusted, and a media.txt cannot name a file outside the zip — it selects from what the archive already holds. media.txt is parsed with no entity resolution, so it cannot reach out to a file or a URL on the way past.
  • Input is bounded. Images are capped at 64 MB, plain text at 64 MB, a .docx body at 128 MB after decompression, a .pptx at 32 MB per part and 128 MB across the whole deck after decompression — a per-part limit alone is no protection when a presentation may hold any number of slides — a playlist at 500 tracks with each capped at 256 MB after decompression, and a PDF at 128 MB on disk with a further ceiling on the text taken out of it — a zip bomb is a few hundred kilobytes on disk, and an app that dies on one is an app that fails the person relying on it to read their post. PDF streams are compressed too, and a malformed one can describe an endless page.
  • A capture file is treated as hostile input, because that is what it is. A .pcap is a recording of what arrived off a network, so every length inside it is attacker-controlled. It is read as a stream and never held in memory; each length is checked against what is actually there before it is used; nothing is allocated on the strength of a number read out of the file; and a packet that ends early yields nothing rather than a panic. The tables the summary is built from stop growing at a fixed ceiling and count the overflow instead — a port scan is half a million one-packet conversations, which is one fact about the capture rather than half a million of them.
  • The names out of a capture are treated as text somebody else chose. Every other figure in a capture summary is counted by the app; the domain names looked up are bytes off the wire, and they are the one part that is both spoken aloud and placed inside a model's prompt. Only the first question of a query is read — never an answer, whose names are compression pointers back into it — and only when every label holds what a hostname may actually contain: letters, digits, hyphens and underscores. The transcript labels that section as text from the file rather than instructions, in the transcript itself and not only in the prompt you can edit. That narrows the opening rather than closing it: a name can still read as a sentence. What it costs if someone manages it is a misleading piece of prose — the counted figures are still there underneath, and the summary says a model wrote the words.
  • Scratch files and directories are created exclusively and, on macOS, readable only by their owner, so a name guessed in advance is an error rather than a write through someone else's symlink. Directories matter as much as files here: the stills taken out of a video are written into one and every image found there is read back and described, so a directory somebody else had already put at that name would be both a copy of what you were watching and a way to put a picture in front of the model that never came out of your video.
  • A network capture is not uploaded even then, unless you allow it. A capture summary is the one thing this app produces that is largely about other people: the machines on a network and the names they asked for. So it is read by a voice on this computer whatever the engine is set to, until Allow captures to be read by ElevenLabs is turned on under Settings — and the Read pane says which voice will read it rather than substituting one silently.
  • Nothing is uploaded except to ElevenLabs, and only when you have chosen it. Images are read by a model on your own machine, and the picture itself never leaves it. TLS certificate verification is never disabled.
  • Your photos' locations stay put unless you ask otherwise. Most cameras and phones write the exact spot a photo was taken into it, and turning that into a place name needs a service — there is no local database of them. So Look up where a photo was taken is off until you turn it on, under Settings → Vision. With it on, the coordinate alone — never the photo — goes to OpenStreetMap's Nominatim and comes back as something like "Ambleside, England", which is then read out with the description.

How it is put together

The binary is called accessengine; the app the user sees is called Speech Output Engine.

File What lives there
src/app.rs Every pane, all UI state, and the keyboard
src/theme.rs Fonts, the contrast-checked palette, and the form metrics
src/dictionary.rs Word replacement
src/jobs.rs Background work and the messages it sends back
src/extract/ .txt, .docx, .csv, .pptx, image and video → text
src/extract/pdf/ .pdf → text: objects, filters, pages, fonts, page description
src/extract/pcap/ .pcap/.pcapng → counted facts: containers, packet decoding, the summary
src/tts/ The ElevenLabs and system-voice engines, and text chunking
src/audio.rs PCM, WAV/MP3 encoding, playback and the transport
src/playlist.rs A zip of audio as a running order, and what media.txt says about it
src/ollama.rs Detecting, installing and calling Ollama
src/ffmpeg.rs Detecting and installing ffmpeg, and taking frames out of video
src/apikey.rs API key storage
src/sysexec.rs Locating system programs, and staging files for them
src/i18n/ The language files, and every word the interface says
src/config.rs Everything else, as JSON

The UI thread never blocks. Anything slow — a network call, a model download, an install, rendering a file — becomes a job on its own thread that reports progress over a channel, which the UI drains once per frame. Jobs know nothing about egui.

Platform differences are confined to src/tts/system.rs and src/ollama.rs, each of which has one module per platform behind a shared signature, rather than cfg scattered through the app.

Licence

MIT. See LICENSE. The bundled Ubuntu Bold font is under the Ubuntu Font Licence 1.0; see assets/fonts/. The four sound effects are CC0 recordings from freesound.org, edited for length and loudness; see assets/sounds/ for what they are and who made them.