# Talking Tom

Turn a PDF, Word (.docx), or text file into a clean spoken-audio file you can
actually listen to.

Drop in a reading, and Talking Tom **cleans it** (strips citations, page numbers,
headers, and OCR noise so they are not read aloud), **voices it** with a free
Microsoft Edge neural voice, and **plays it back** with a waveform scrubber and
pitch-preserved speed control. macOS desktop app, single Python file, no build step.

**English and Spanish.** On first launch the app asks which language you want; the
whole interface is in that language (change it later in **Settings → Language /
Idioma** - the app relaunches to apply). The app name and cat icon stay the same
in both.

**Interface language and content language are independent.** Two ways to get
audio in the other language, regardless of what the buttons say:

- **Translate** button (next to *Clean Only*) - detects whether the text is
  English or Spanish, translates it to the other one in place, and sets **Read
  in** to match so *Generate Audio* voices it correctly. Needs a Claude key.
- **Read in** dropdown - `Auto (source)` / `English` / `Spanish`. On a specific
  language the cleaning pass translates as it cleans, and the voice list switches
  to that language's Edge voices. `Auto` keeps the document's own language.

Claude is natively multilingual, so translation works the same in both
directions. Language detection is a simple word-frequency heuristic
(`_guess_lang`).

> **Note:** "Talking Tom" is also the name of an Outfit7 game. This is an
> unofficial personal project, not affiliated with or endorsed by Outfit7. Ship
> it with the "unofficial / not affiliated" disclaimer the way the other tool on
> the site notes it is unaffiliated with Anthropic.

---

## Requirements

- **macOS** on Intel or Apple Silicon.
- Internet for the one-time setup, for the voices (a free Microsoft endpoint),
  and - only if you turn on AI cleaning/translation - for the Anthropic API.
- **No Python needed.** `setup.command` installs its own via
  [uv](https://astral.sh/uv) (a uv-managed CPython build that includes Tkinter).
- **No ffmpeg required.**

## Install

1. Unzip `talking-tom-1.0.0.zip`.
2. **Right-click `setup.command` -> Open** (once - macOS blocks a plain
   double-click on downloaded scripts; the right-click menu adds an **Open**
   button that gets past it). Or from a terminal: `chmod +x *.command &&
   ./setup.command`.
3. It bootstraps `uv`, installs a private Python + the dependencies into
   `~/.talking_tom/`, and builds `~/Applications/Talking Tom.app` (with a Desktop
   alias). ~2 minutes. The downloaded folder can be deleted afterward.

Re-run `setup.command` any time to upgrade in place; it keeps `config.json`.

`run.command` is a fallback launcher (`./run.command ~/Desktop/paper.pdf` opens a
file on launch); normally you'd just open the app from Applications.

## Get an Anthropic API key (optional)

AI cleaning is the only part that costs money, and it is optional - without a key
the app falls back to a fast built-in rule-based cleaner that is completely free.

1. Sign in at **console.anthropic.com**.
2. Add a little credit under **Billing** (a few dollars lasts a long time - a
   typical paper cleaned with Claude Haiku is around 1-8 cents).
3. **API keys -> Create key**, copy the `sk-ant-...` value.
4. Launch the app, click **Settings**, paste the key, pick a model, **Save**.

The key is written to `~/.talking_tom/config.json` with `600` permissions and is
never sent anywhere except Anthropic. You can also set `ANTHROPIC_API_KEY` in your
environment instead.

## Use

```
./run.command                     # launch
./run.command ~/Desktop/paper.pdf # launch and start converting that file
```

- **Listen to a document** - pick a PDF, `.docx`, or text file; it is cleaned,
  voiced, and starts playing in one step. Or drag a file onto the window.
  (Old binary `.doc` isn't supported - save as `.docx` first.)
- **Paste text** into the box and hit **Generate Audio**.
- **Voice / Speed / Cleaning / Read in** dropdowns set the voice, the generated
  speaking rate, how hard it cleans (`AI (Claude)` / `Built-in` / `Off`), and the
  output language (see above). Interface language is in **Settings**.
- **Clean Only** transforms the text box without making audio; **Translate**
  swaps English<->Spanish in place.
- The line above the text box is a live **estimate**: word count, listen time,
  and - when AI cleaning is on - the approximate Claude cost for this run
  (`<1¢`, `~4¢`, ...). Audio generation is always free. Runs estimated over ~50¢
  ask for confirmation first.
- **Player** - click or drag on the waveform to scrub, `±10s` buttons, and a
  speed slider (0.5x-2x) that preserves pitch. The slider works from any speed
  and no matter what rate the clip was generated at (it decodes to the mixer's
  sample rate and time-stretches in memory; the swap happens with the old audio
  still playing so there's no gap).
- Output MP3s and their transcripts are saved to `~/Talking Tom/`.
  **Save Audio As...** (Cmd+S) exports a copy elsewhere.

## Where things live

| Path | What |
|---|---|
| `app/talking_tom.py` | the entire application |
| `app/header.png`, `app/icon.icns` | in-window badge + `.icns` app/launcher icon (the cat) |
| `app/make_icon.py` | rebuilds `icon.icns` + `header.png` from `logo_src.png` (not shipped - drop one in, or it draws a headphones mark): `python make_icon.py <out_dir> [source.png]` |
| `~/.talking_tom/app/`, `~/.talking_tom/venv/` | where `setup.command` installs the app + its private Python |
| `~/.talking_tom/config.json` | saved settings + API key (created on first run, `600` perms) |
| `~/Applications/Talking Tom.app` | the launcher `setup.command` builds (`osacompile` applet -> runs the venv python) |
| `~/Talking Tom/` | generated `.mp3` files and `.txt` transcripts |

## The stack

| Layer | Choice | Notes |
|---|---|---|
| Packaging | `setup.command` + **uv** | uv installs a private CPython (Tkinter included), builds the venv, and `osacompile`s the `.app`. No system Python, no code signing (the `.app` is built locally so it isn't quarantined) |
| UI | **Tkinter / ttk** | stdlib; custom-drawn rounded buttons and waveform canvas because Tk ignores colors on macOS |
| i18n | `STRINGS` dict (`en`/`es`) + `t()` | interface only; code stays English. First-run prompt (`_first_run_language`); language change re-execs the process |
| Content language | `readin` setting (`auto`/`en`/`es`) + `_guess_lang()` | independent of the UI; drives translation + voice list |
| Drag & drop | **tkinterdnd2** | optional; window still works without it |
| Text extraction | **pypdf** + **python-docx** | PDF and `.docx` (paragraphs + table cells) -> text; `.txt` read directly. Both optional; missing one just disables that format |
| Cleaning (offline) | regex rules in `HeuristicCleaner` | English + Spanish abbreviation / punctuation maps (`lang` arg) |
| Cleaning + translation (AI) | **anthropic** SDK, Claude Haiku 4.5 or Sonnet 5 | chunked ~9k chars/request, lazy import; `TTS_CLEAN_SYSTEM` has `en`/`es` prompts, `TTS_CLEAN_TRANSLATE` adds clean-and-translate prompts |
| Text to speech | **edge-tts** | 8 English + 6 Spanish Edge neural voices, rate control, retry/split on failure; free |
| Playback | **pygame.mixer** | streams the MP3 at 1x |
| Speed change | **numpy** | WSOLA time-stretch in `time_stretch()` - tempo without pitch shift. `decode_int16()` resamples to the mixer rate so generated (24 kHz) clips play at the right pitch; `set_speed()` prepares the new audio before halting the old |
| Waveform | **numpy** + pygame sample buffer | amplitude envelope, no ffmpeg |
| Duration probe | **mutagen** (MP3), **pydub** (other) | both optional |

## Cost model

- **Text to speech: free.** edge-tts uses a public Microsoft endpoint, no key.
- **AI cleaning: paid, optional.** Estimated in-app before each run from a
  chars/4 token estimate against list prices (Haiku 4.5 $1/$5 per MTok,
  Sonnet 5 $2/$10 per MTok), counting the system prompt once per chunk and
  assuming output ~90% of input length. It reads slightly high on purpose.
  See `estimate_clean_cost()` in `app/talking_tom.py`.

## Troubleshooting

| Symptom | Fix |
|---|---|
| `setup.command` won't open ("Apple could not verify...") | right-click -> **Open** (adds an Open button), or `xattr -dr com.apple.quarantine <folder>` then run it |
| setup fails downloading uv / packages | no network, or a proxy - connect and re-run `setup.command` |
| "No Claude key yet - used the built-in cleaner" | expected without a key; add one in Settings for AI cleaning |
| Voices fail / time out | transient Edge outage or no network; it retries 3x per chunk |
| Speed slider snaps back to 1.0x | clip longer than 1 hour, or the decode failed - too long / not stretchable in memory |
| the `.app` won't open after setup | rare; run `~/.talking_tom/venv/bin/python ~/.talking_tom/app/talking_tom.py` or `run.command` and check `~/.talking_tom/last_run.log` |
