tts-voice-creator-clone-and.../Manual.md
mARTin-B78 772c43d7f0 feat(voices): LibriVox and Wikimedia Commons voice sources
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:42:36 +02:00

816 lines
82 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# User manual
Living manual. Every feature updates this file in the same commit as the code.
> Sections for the mature features (voice library, Read Aloud, audiobook
> casting, Rehearser) are still to be written — this file was created alongside
> the magazine feature and currently documents that area only.
---
## Inline tags in the text (optional)
Purpose: pauses, spelled-out acronyms and slower/faster passages inside one text. Prerequisite: switch on **Settings -> General -> Inline text tags** (default off; when off the tags are spoken literally). A syntax hint sits under the text boxes in Try It Out and Read Aloud.
| Tag | Result |
|---|---|
| `[pause 500ms]` / `[pause 1.2s]` | silence, max 10 s each and 60 s per text |
| `[spell]USA[/spell]` | letters one by one (German letter names for German text) |
| `[slow]..[/slow]` / `[fast]..[/fast]` | 0.85x / 1.15x speed, same pitch; innermost wins; unclosed ends at the paragraph |
Known control tags such as `[laugh]` are removed, never read; `[1]` and `[sic]` stay. Rendered (pauses, spell, speed) in: Try It Out/generate, Read Aloud and audiobook line previews (both call `/api/tts-preview`), the OpenAI-compatible `/v1/audio/speech` (non-streaming), and `/speak` and the MCP speak tool. Tags are only removed, never spoken, in: streaming playback and streaming `/v1/audio/speech`, spoken chat replies, and magazine MP3 (so recorded sentence timings stay exact). Speed uses ffmpeg, so no engine support is needed. Errors: a failing engine reports the usual TTS error. Mobile/accessibility: the switch is a labelled checkbox with `role=switch` (keyboard Space, 44 px row); German and English labels. Privacy: text stays local.
## Design a Voice: attribute chips
**Purpose:** build a voice description quickly. **Use:** open Design a Voice, press chips (gender, age, pitch, pace, accent/region, tone/timbre, emotion). Each chip adds its phrase to the description; pressing it again removes only that phrase. Gender, age, pitch and pace allow one choice each. **Free text:** type anything in the box; chips never overwrite your text and are not a limit on what you can describe. **Keyboard/touch:** chips are buttons (Tab, Space/Enter, `aria-pressed`, 44 px targets). **Languages:** chip labels follow the UI language (German/English); the inserted phrase is English. **Offline/privacy:** purely local, nothing is sent until you generate.
---
## Remove background music from URL imports (optional)
- **Purpose:** strip music and background sound from a downloaded clip before cloning, so the clone does not learn the music.
- **Prerequisites:** off by default. The server owner sets `VOCAL_ISOLATION=1`, installs Demucs (`pip install demucs` in a venv or sidecar, ARM64 supported; set `VOCAL_ISOLATION_CMD` if it is not on `PATH`) and downloads the `htdemucs` weights once (`955717e8-8726e21a.th`, about 80 MB, into `$TORCH_HOME/hub/checkpoints`). The app never downloads weights itself. Without all of this the button does not appear.
- **Steps:** download a YouTube/URL clip, press **Remove background music**, wait (minutes on CPU). The cleaned audio loads in the player or waveform; press **Play original** / **Play without music** to compare. Then trim and save as usual.
- **Result:** a new temporary audio; the original is kept until it expires.
- **Errors:** clips over 15 minutes are refused (trim first), only one job runs at a time, a timeout or failure shows a message under the button.
- **Mobile/accessibility:** buttons are normal buttons; progress and errors are announced through a polite live region.
- **Offline/privacy:** runs fully locally; no audio leaves the machine.
## Voice export and import bundles
Purpose: back up a voice or move it between machines (DGX and NUC) as one `.voice.zip` file containing the reference audio, transcript, settings (seed, design prompt, rating, tags, source & consent, audio problems), and optionally the picture and the consent recording.
Prerequisites: Voice library open. Export needs at least one checked voice.
Steps: tick the checkboxes of the voices, press "Export selected"; one `<voice>.voice.zip` is downloaded per voice. On the other machine press "Import bundle" and choose one or more `.voice.zip` files.
Expected result: the voice appears in the library. If the name is already taken the import gets a suffix (`_2`, `_3`); an identical voice that is already there is reported as "already present". Existing voices are never overwritten.
Errors: damaged, tampered, oversized or unexpected files are rejected with a message (for example "Hash mismatch"); nothing is imported then. Messages appear as toasts, which screen readers announce.
Mobile / accessibility: both buttons are at least 44 px high and keyboard operable; the file chooser is the browser's own.
Offline: works fully offline. Privacy: a bundle contains the voice, its source/consent notes and any consent recording; share it only with people you trust.
## LibriVox and Wikimedia Commons voices
Purpose: find short speech samples with an open licence and import a clip as a voice.
Prerequisites: Voice library; internet access from the server.
Steps: Get Voices Online -> tab "LibriVox & Wikimedia"; choose source, type a term (LibriVox matches the start of a title; the language filter works for LibriVox only), press Search (can take about 15 s). Play the preview, set clip start and length (seconds), press "Import clip".
Result: the clip is saved as a voice; its "Source & consent" shows source page, licence and, for Wikimedia, the attribution text. LibriVox is recorded as public domain with a reminder to check the reader's licence; Wikimedia as licensed (or public domain).
Errors: shown in the status line under the search box (bad input, source unavailable, trim outside the audio).
Licences: CC BY / CC BY-SA need attribution when you publish results; read `architecture/voice-sources.md` before publishing anything cloned from these voices.
Mobile/accessibility: 44 px controls, labelled fields, status is announced, previews are native audio players.
Offline: needs internet. Privacy: your search term goes to librivox.org / archive.org or commons.wikimedia.org / upload.wikimedia.org; nothing else.
## Voice source & consent
Purpose: record where a voice comes from and why you may use it (own voice, speaker consented, licensed, public domain, unknown).
Prerequisites: a voice in the Voice library. For a spoken consent statement: a microphone and browser permission.
Steps: open a voice in the Voice library, scroll to "Source & consent", pick the basis, add source (URL or description, never passwords), licence and a note, press "Save source & consent". Optionally press "Record spoken consent", say who you are and what you consent to, press "Stop recording"; the clip is saved and can be replayed.
Result: the "Source unknown" warning badge disappears from the list once a basis other than "Unknown" is saved. Imported voices get the source URL prefilled but stay "Unknown" until you decide.
Errors: invalid values (bad basis, over-long text, non-http(s) URL or URL with credentials) are reported next to the Save button. If the microphone is denied or missing, type a written consent statement into the note field instead.
Mobile/accessibility: all controls have labels and at least 44 px targets; status messages are announced; the typed note is a full alternative to recording.
Offline: works offline (local server only).
Privacy: everything stays on this server in the voice's `.meta.json` and `<voice>.consent` file; nothing is sent to external services. Generation is not blocked by this field (yet).
## Voice health: audio problems
**Purpose:** reference audio is checked for `silence` (over 1.5 s inside the clip), `clipping` (peaks at full scale for 0.1 s or more) and `level jump` (15 dB or more between adjacent half-second windows).
**Where:** (1) Settings, Routing, Test: the result line shows "Audio problems: clipping at 2s-2.4s (peak at 0.0 dBFS); ..." (first 8). (2) Try-out, after selecting a library voice: the voice panel shows the same text under the warnings (screen-reader announced; wording carries the meaning, not colour). Each kind also adds a warning.
**When computed:** on import (stored in the voice's meta as `audio_problems`), and fresh on demand via `GET /api/voices/{id}/health` (cached by file modification time). The voice list only returns the stored result.
**Errors:** unreadable audio yields no problems; only the first 120 s are scanned. **Privacy/offline:** local analysis only.
## Seed Finder: Auto-pick best
**Purpose.** Finds a good seed for a voice without listening to every sample first.
**Prerequisites.** A voice with a reference clip (Voice Clone backend); a speech-to-text engine is optional.
**Steps.** Open the voice, then *Seed Finder*. Set the test sentence and the seed range (at most 16 seeds are tried), then click *Auto-pick best*. The server generates each candidate one after another, scores it and the best row is marked "Best". Play any row, then click *Use seed N* to save it. Nothing is saved automatically.
**Score (0-100).** Weighted mean of: spoken-back word accuracy (50%, text read back by speech-to-text), loudness and clipping (20%), audio problems such as long silences and sudden level jumps (30%). Weights and penalties are constants in `core/seed_score.py`.
**Errors.** If speech-to-text is down, the result says so and scores by loudness and audio problems only. A failed seed shows its error and is skipped. *Cancel* stops after the running candidate; the voice configuration is always restored.
**Mobile and accessibility.** Progress and the result are announced in a polite live region; the button is keyboard operable. German and English labels.
**Offline.** Needs the TTS server (and optionally the STT server) on your network. **Privacy.** The sentence is sent only to your own TTS/STT servers.
## Audiobook casting (Read Aloud → Cast as Audiobook)
### Purpose
Turn a book into a script with one narrator and one voice per character, ready for
the Rehearser and MP3 export.
### Prerequisites
- A book open in Read Aloud (PDF or text) and an LLM configured in Settings.
### Step by step
1. Open the book, then **Cast as Audiobook**.
2. The app finds the quotation marks in the text and splits it into narration and
spoken lines by itself — this part never changes a word of the book. The LLM is
then asked only *who speaks* each spoken line, in passages of a few thousand
characters. A whole novel takes roughly ten minutes (the previous method took
hours).
3. Progress shows per passage; **Stop Casting** keeps what is done and **Continue
casting** resumes.
4. Review the cast as before. **Recast unknown** re-reads only the lines still marked
Unknown, with more context; a line the model cannot place stays Unknown rather than
getting a guessed voice. **Verify** (the second quality run) reads every spoken line a
second time with different passage boundaries — about as long as the first cast — and
**never changes a speaker**: lines both readings agree on are marked confirmed (about
4 in 5 of those were right in testing), and lines where they disagree keep the first
reading, are flagged in the review list and show what the other reading said. The
percentage next to each line means what it says in testing: confirmed lines were about
82 % right, disputed lines about 48 %, lines not yet verified about 76 %. Use the flags
to decide where to spend your review time. Neither step can change the book's
text, and a stopped or failed run leaves the cast as it was.
**Expected result:** every word of the book appears exactly once, quoted speech is
typed as dialogue and everything else as narration. When the text only refers to a
person by role ("der Fremde", "die Baronin"), the line is labelled with that role;
later passes merge it into the named character when the book itself ties the two
together (e.g. "der Inquisitor Marcian"). Some role labels stay separate — use the
character merge in the cast view for those.
### Errors, limits and privacy
- If the LLM cannot be reached for a passage, its lines are named from the speech
tags around them where possible and otherwise marked Unknown; nothing is lost.
- A book whose speech is not marked with quotation marks (»…«, „…“, “…”, «…») uses
the previous whole-passage method automatically. To force the previous method in
this browser: `localStorage['ab-span-mode'] = '0'`.
- The custom attribution prompt in Settings only applies to the older whole-passage method; span
mode uses its own fixed instructions (it only asks who speaks each line).
- Book text goes to the LLM you configured, exactly as before; nothing else is sent
anywhere. Nothing works offline without a reachable LLM.
- Accuracy: measured against hand-corrected casts of three novels, roughly 90–92 % of
all words end up with the right voice and about three quarters of spoken words with
the right speaker. Similar-looking characters in the same scene are the usual
mix-up; check those in review.
### Character concept sheet (Konzeptbild)
**Purpose:** one picture per character for costume continuity: the character from the
front, the side and the back on top; below, the same character in a moment from the
book, and their own items (up to three, e.g. Marcian's crossbow).
**Prerequisites:** a local ComfyUI (Settings > Engines > Image Generation) and the
character's Image and Concept Art prompts (Cast Studio > character > Generation Prompts).
**Step by step:** in Cast Studio tick the characters and choose **Consistent profile +
reference**, or open a character and regenerate its Konzeptbild. Pick *Cinematic* or
*Concept art* in the style menu first.
**Expected result:** the lower pictures show the same person as the poses (face, hair,
armour or clothing, colours), in the medieval high-fantasy world of the book (stone,
timber, tents, torch or daylight, other people in medieval dress), and the item lies on
a plain neutral background as an isolated prop, with no hands, feet or other body parts, made in the style and colours of its owner's costume. They contain
no writing, captions or modern things. The face in the story moment is taken from the
profile picture.
**Errors and limits:** the fast local model still gets details wrong now and then (a
crossbow with a misshapen stock, a second weapon in the background, *Concept art* coming
out close to photographic). Regenerate the sheet; each run uses a new seed. The automatic
image check looks at gender, ears and wounds in the story moment, not at modern objects
or text, so look at the lower pictures yourself. Sheets made before 2026-10-02 keep their
old lower pictures until they are regenerated.
**Species:** the body shape (for example short dwarf proportions with a full beard) follows
only the species the character's own *Race / Species* field states first. A note in
brackets such as "Mensch (oder Zwerg, je nach Kontext …)" or a long remark about other
characters is ignored, so such a character is drawn as a human. If a character really is
a dwarf but the field says "Mensch (…)", change the field to "Zwerg" and regenerate.
**Story moment:** the lower picture uses a quote from the character's *Sources* that
names this character (by name, real name or a real alias such as "Murlok von
Mengbilla"; "der Zwerg" alone does not count). A quote that only names someone else is
never used; without a fitting quote the character's own mannerisms or abilities are
shown instead. To change that fallback, edit the character's *Mannerisms*.
**Age:** a child or teenager is drawn as one. The app reads the age estimate ("12
Jahre", "zwölfjährig", "fast acht Sommer", "15 years old"), age quotes in *Sources*,
and words like "Kind", "Junge", "Mädchen", "boy", "girl". "Junger Mann" / "Junge Frau"
stay adults. If a hedged estimate ("Junges Mädchen / Junger Mann") is drawn wrongly,
write the age as a number in *Estimated age* and regenerate.
**Privacy and offline:** only the prompts and the profile picture go to your own
ComfyUI; nothing leaves your network. Without a reachable ComfyUI nothing is generated.
**Accessibility and mobile:** the sheet is shown as one image with the character's name
as its alternative text; the character's written profile next to it carries the same
information.
### English prompts for character images
**Purpose:** the image models work best with English, but character sheets from German books are German.
Before a profile or reference-sheet prompt is built, the app translates the character's text (species,
appearance, clothing, age, items, story moment, the book's setting) to English with your own LLM.
**Prerequisites:** the LLM from Settings. Without it the original German text is used.
**Usage:** nothing to do. Generate prompts or images as before. Your character sheet keeps its German text
and is never changed; translations are cached, so a character is translated once and again only after its
text changes. To inspect the result without generating anything, call `POST /api/character-prompt-preview`
with a character `id`.
**Expected result:** English prompts; the response and the finished image job contain `prompt_translation`.
**Errors:** if the LLM is unreachable or its answer fails validation, the original text is used for the
affected fields, a warning is logged, `prompt_translation.used_untranslated_text` is `true`, and generation
continues. Set `PROMPT_TRANSLATION=off` to disable translation entirely.
**Mobile, accessibility, offline:** no interface change. Offline (no LLM) falls back to German text.
**Privacy:** character text goes only to the LLM you configured; cached translations stay in the local database.
### Distinct faces for each character
**Purpose:** characters of one book used to look like members of one family, because most sheets describe no face
and the image model then draws its default one. Every character now gets its own face description, so the cast is
easy to tell apart. What the book says about a character always wins.
**Prerequisites:** none. It is part of generating character prompts and images.
**Usage:** generate profile/reference images as before. The prompt now contains a `face:` clause such as
`face: oval face, narrow pointed chin, straight nose, wide-set grey eyes, ..., chestnut hair, shoulder-length wavy hair`.
Parts the sheet states (for example "schwarzhaarig, blaue Augen") are kept; the other parts are chosen by the app,
fitting species, age and gender (no beards for women or children, no elf traits on humans, grey hair for the old),
and spread across the characters of the book. The choice is stable: the same character always gets the same face,
and adding new characters does not change existing ones. Relatives named in the relationships field (brother,
father ...) may share eye colour, hair colour, nose or jaw.
**Override:** to fix a face, write it in the sheet field `visual_face` (free text, English works best), for example
`narrow face, hooked nose, one pale grey eye, black hair tied back`. It replaces the generated face. Describing
features in the physical appearance also works; those words are kept.
**Preview without generating:** `POST /api/character-face-preview` with `{"book": "<book title>"}` returns every
character's face (and where each part came from) plus a similarity matrix and a before/after score;
`POST /api/character-prompt-preview` shows the finished prompt including `face_clause`. Nothing is generated or saved.
**Expected result:** clearly different faces within a book. Existing images stay as they are until you regenerate them.
**Errors:** if the cast cannot be read, the character gets a face from its own sheet alone. Non-human sheets (a raven,
the narrator) get no face clause.
**Mobile, accessibility, offline:** no interface change; works offline (no LLM needed for the face itself).
**Privacy:** nothing leaves the machine; profiles are derived from the stored sheets and not saved.
### Keeping characters apart (profiles and the character library)
**Purpose:** each character's profile holds only that character's facts.
**What the app does:** two profiles are treated as the same person only when they share
a real name. Labels many people share ("Zwerg", "der Zwerg", "Mann", "Fremder",
"Soldat", "Ork-General") never join profiles. A profile is saved to the record with
exactly its name. When the cast is known, another cast member's name is never kept as
an alias or first/last name (relations such as "Tochter des Promos" stay), and notes
in which the model talks to itself ("Warte: ...", "Der Text sagt ...") are removed.
**Checking an existing library (administrators):** on the server, run
`python3 scripts/audit_character_records.py` for a read-only report of copied profiles,
names of other characters in aliases, leaked notes and foreign quotes. To see what a
cleanup would change, run `python3 scripts/clean_character_records.py --out <folder>`:
it writes `report.md` and changes nothing. Only with `--apply` (after you have read the
report) does it save a backup to the folder and change the listed records; copied
profiles are only reported, you decide which one to keep (delete or recast the other).
**Errors:** a profile that was copied or mixed up before this fix keeps its wrong text
until you edit it, delete the copy, or recast that character.
**Mobile, accessibility, offline:** no interface change. **Privacy:** the scripts run
locally against your own database; nothing is sent anywhere.
---
### Characters: all books or one book
**Purpose:** two views of the same character library, so you always know which book you are working in.
- **All characters (all books)** (sidebar *Library > Characters / Cast*, or the Library *Cast* tab): every character of every book, one band per book with a heading and a count. The first book is open, the others are closed and only drawn when opened. *Search all books* (name, alias, archetype, book) opens every band with matches and shows "2 of 9". Cards/Table, Sort and every per-book action stay in each band header.
- **Studio > Voices** (step 3): after you pick a book in *Source*, only that book's characters are shown, under a **Book: <title>** chip. **Show characters of all books** leads to the global view. The Characters step (2) shows the same chip. With no book picked, Voices offers **Pick a book** and the link to the global view instead of listing every book.
**Errors:** if the book title cannot be read, the prompt to pick a book is shown. **Mobile/accessibility:** bands are headings with a show/hide button (`aria-expanded`); the search field is labelled; layouts fit 320 px. **Offline/privacy:** nothing new is sent; the view reads the local library.
## Magazines
### Purpose
Listen to a magazine or newspaper one **article** at a time, from its true
beginning to its true end, without adverts, sidebars or a neighbouring article
spliced into the middle. Each article can be narrated by a single voice, or
turned into a podcast discussion between two or three voices you pick.
### Prerequisites
- The magazine as a PDF (native text or scanned — both work), **or** its pages as PNG/JPEG images (scans or phone photos): choose or drop several at once and they become one magazine, pages in filename order (`page2` before `page10`). Up to 300 images of at most 25 MB each; a photo's EXIF rotation is applied. Images carry no text, so every page is read with OCR. Do not mix a PDF with images.
- At least one voice in the library for narration.
- For podcast mode: an LLM configured in Settings (`llm_url`, `llm_model`), and
two or three voices chosen as hosts.
### Step by step
1. **Add the magazine.** Studio → *Add Magazine / Paper*. Give it a title and
issue, then upload the PDF.
2. **Ingest.** Ingestion runs in the background — you can close the browser. It
renders every page, extracts text (falling back to OCR on scanned pages),
classifies each region of the layout, finds the contents page, and proposes
articles with page ranges. While it runs, a live preview shows each page's
image next to its recognised text as soon as that page finishes -- a
detected headline is shown larger, a figure appears as its own picture
marker rather than as garbled on-image text, and page furniture is shown
dimmed -- so you can see ingestion actually working page by page instead
of watching a bare progress bar. This preview is a quick approximation,
not the final read: it runs before the layout is fully classified, so a
heading guess or an excluded marking here can still change once review
opens. The page image also shows the **regions as they are found**: each
box is drawn over the page, coloured by type (headline, paragraph,
caption, picture, advert, running head …, see the legend under the page)
and numbered in reading order. After all pages are read, the second pass
(*Recognising paragraphs, headings and advertisements*) walks the pages
again and the preview follows it, so you can watch boxes change type —
the progress bar then reads "… — page 12 of 204". Pictures, charts,
tables and code are only recognised in this second pass (the first pass
only reads text, and says so under the page); they are drawn as dashed,
filled boxes in their own colours — picture orange, chart green, table
yellow, code grey — so they stand out from the text boxes, and fitted
to the actual picture as soon as that page is labelled (not only once
the whole magazine has finished), so the box does not lag behind the
real edges of the picture for long. The recognised-text panel shows the
actual cropped picture, chart or table for the same reason, rather than
a plain "Figure" placeholder, as soon as it has been cropped. The page
scan itself (a scanned magazine is one picture per page) is not framed.
A proposed picture, chart or table is checked against the page render
itself before it is created, so a text area or column gap is not shown
as a picture. A real picture can still get a loose or shifted box in
some cases; use *Refine figures* (or re-run detection on the page) to
try snapping it again, or fix the box by hand on the review page. If a run is interrupted
(for example when the server restarts), the status now says so instead
of spinning forever; start it again with *Re-run detection*.
3. **Review the articles.** This step is not optional and cannot be skipped.
Each proposed article shows its title, author, page range, estimated
listening time and a confidence score. The review screen is split: the
scanned page on the left with a numbered, icon-labelled box per detected
region (title, byline, body text, caption, figure…), and the same regions
as text on the right — click either side to highlight the other. Sidebars
and pull-out boxes are already unticked for you — tick one back in if you
actually want it read. A region already claimed by a different article on
the same page is marked with that article's number, so it is never
mistaken for unclaimed text. If OCR quality is poor, **"OCR again"**
re-reads just the current page through the configured vision engine
(or **"OCR again (whole magazine)"** from the contents screen for every
page, which also resets any articles already reviewed). If a figure's
box is drawn in the wrong place or at the wrong size — a common miss,
because an OCR engine locates a photo far less precisely than it reads
type — **"Fix figure boundaries"** re-derives every figure box in the
issue from the scanned pages themselves. It calls no OCR engine, changes
no text and touches no approval, so it is safe to run at any point,
including on an issue you are halfway through reviewing; boxes it cannot
place confidently are left exactly as they were. If the layout
engine missed a region entirely, **"Draw it"** lets you click-drag a box
directly on the page image and add it as text (typed or OCRed) or as a
new figure. If a region was found but its box is misplaced, the wrong
size, or shouldn't exist at all, **"Edit boxes"** lets you drag an
existing box to move it, drag any of its eight edge/corner handles to
resize just that side, or press its **×** to delete it outright. The
per-edge handles matter most when two regions share a boundary — for
example a paragraph split wrongly between two boxes — since dragging just
the top or bottom handle moves that one shared line without disturbing
either box's width or its opposite edge. Moving or resizing opens a small
confirm panel — **Save** keeps the region's existing text at its new
position; for a text region, **Save & re-OCR** also reads the corrected
area again and replaces the text with that fresh reading (useful when the
box was too small or too large to read cleanly the first time, or after
moving a shared boundary). Deleting a region removes it from the page
entirely, not just from the article currently under review. Every row in
the list on the right also has its own **type picker** and **delete
button** (the bin icon), so you can fix a region without switching into
Edit boxes: change a paragraph into a *Headline*, a *Page number*, an
*Advertisement* or any other kind, or turn a text box into an *Image*
(the box is cut out as a figure) and back (the box is read with OCR).
Delete asks for confirmation first. The page's reading order is updated
after a type change, because a page number or advert is read differently
from running text. **"Fix reading order"** re-reads the order of every
page in the issue column by column — left column top to bottom, then the
next column — for issues imported before the column detection improved.
It changes only the order: text, boxes and approvals stay as they are,
although an approved article may now be read in a different (correct)
order. Short pull-quotes are read at the end of the page instead of in
the middle of a sentence. To **set the order yourself**, either type a new
number into the position box at the front of a row (the region moves
there), or press **"Order by clicking"** and click the boxes on the page
in the order they should be read — each clicked box shows its new number,
*Undo last* takes one back, and boxes you skip follow in their current
order — then *Save order*. A page ordered by hand says so above the list
and is never reshuffled by *Fix reading order*, a type change or a newly
drawn box (a new box is read last until you move it); **"Automatic
order"** hands the page back to the automatic column order.
While you order, a panel pinned to the bottom-right of the window shows how many boxes are numbered and holds **Save order**, **Undo last** and **Cancel** (or press **Esc**). It stays in view however far you scroll, sits above the Back / Approve bar and clear of the page image, and turns green when every box is numbered. **Save order** is only enabled once you have clicked at least one box; on a phone the panel spans the width of the screen.
**"Clean up boxes"** fixes four problems across the whole issue at once:
short text inside a picture (a label on a screenshot, a word on a slide in
a photo) becomes *Text in picture*; boxes of one or two characters become
*OCR noise*; a box that only repeats a bigger box's text (typically a
subheading the paragraph box also read) becomes *Duplicate*, so it is not
read twice; and an author line that runs into the article's first
sentence is split into the name and a paragraph of its own. None of these
are read aloud, nothing is deleted, every change shows in the list and can
be undone with the type picker, and a backup of the page layout is kept
(`layout.backup-<date>.json` in the issue's folder). New imports and
*OCR again* do this automatically. Longer text inside a picture is left
for you to judge: it can be real text caught in a picture frame that is
too big. Each text row also has three quick actions: **Edit text** (the
pencil) opens the region's text in place — fix OCR mistakes and *Save
text*, or put the cursor where one paragraph ends and press **Split
here** to make two regions (the second is read right after the first);
**Read again** re-runs OCR on just that region; and **Merge with the next
region** joins a paragraph the OCR cut in two (a word broken by a hyphen
at the line end is closed up again). Everything works from the keyboard
too: ↑/↓ move between regions, Space ticks or unticks, Enter edits the
text, R reads again, M merges with the next region and Delete deletes
(with Undo); letters typed in the text editor are never taken as
shortcuts. To fix **many boxes at once**, press **"Select boxes"**, then
click boxes on the page or drag a rectangle over them (every box at least
half inside is selected; X selects the focused row from the keyboard). A
bar shows how many are selected: change their type in one go, or delete
them. Deleting, changing a type, merging and splitting no longer ask
"are you sure?" — they happen at once and an **Undo** button appears
above the list (it undoes the latest change, and only while nothing else
has changed since). When a region becomes a picture — drawn as one or
changed into one — the short text inside it (a chart's legend and axis
labels) automatically becomes *Text in picture*, so there is nothing left
to delete by hand. Use
**"Two-page spread"** to see this page next to its facing page (e.g. 22
and 23), each with its own OCR-results list directly beneath it — useful
for lining up a figure or a jump that crosses the gutter. Only one page
is editable at a time; click **"Edit this page"** on the other half to
switch which one the drawing and editing tools apply to. Confirm the
boundaries, correct the title, then **Approve**.
**Approve** on one page of a multi-page article keeps the blocks of all its pages: only boxes you untick or add change what the article contains.
**Untitled entries ("Page 33") in review.** An entry without a real title shows its page label (for example "Seite 33") and a suggestion box:
- *Merge into previous article* appears when the page looks like the continuation of the article before it. Open **Why do we think so?** to read the reasons in plain words and the confidence. Press the button to merge. Use **Undo** in the bar above the page to take it back. If the suggestion changed in the meantime nothing is merged and the suggestion is refreshed.
- *Use suggested title* writes the proposed title into the entry (never over a title you set). Undo is offered.
- After a merge the article's audio is marked *Audio is out of date: re-render*; render it again from the contents list. No audio file is deleted.
Page labels and these badges also appear in the contents list, the playlist and the driving-mode index. Mobile: buttons are full width and 44 px high; keyboard: Tab to the buttons and the details summary, Enter or Space to use them. Privacy: nothing leaves the device beyond the local server.
Optionally, click **"Describe"** under a figure, chart, table or code block
to have the configured vision engine generate a one- or two-sentence
description for listeners who cannot see the image — e.g. what a chart
measures and its general trend, or a table's row/column headings and
what stands out. You can edit the generated text, or write your own from
scratch, before approving the article. Generating a description changes
nothing about existing narration by itself — it is only spoken if you
also turn on "Read figure descriptions" in step 4.
4. **Listen.** An approved article can be narrated with the default voice, or
sent to podcast mode. Pick the narrator (and the podcast hosts) from the
same searchable voice picker used elsewhere in the app — open it and type
to filter the list. Playback synthesises one sentence at a time, staying a
few sentences ahead, so you only wait for the first. To avoid waiting at
all, press **Prepare audio** first: it synthesises the whole article now
(starting from where you are, then the part before it), shows progress as
"Preparing audio 12 / 24", and can be cancelled with the same button. Once
it says "Audio ready", **Listen** plays straight through from the prepared
audio. Prepared audio is held in the browser only — closing or reloading
the tab, opening another article, or changing the narrator discards it, and
Prepare audio must be run again. Use **Play all articles** (on the contents screen) to
listen through every approved article back to back without returning to
this screen between them. A figure from the page is shown automatically as
playback reaches it. **Word highlighting** can be turned off if you find it
distracting — it is an estimate based on word length, not a measurement, so
it will not always land exactly on the spoken word. Turn on **"Read figure
descriptions"** in the reading controls to have any figure descriptions
generated during review spoken aloud where they occur in the article;
figures without a generated description are silently skipped, exactly as
before this option existed. **Driving mode**
(the button next to the article title) is a full-screen, podcast-style player
for the generated MP3s: previous/next article, **back 15 s / forward 15 s**,
large play/pause, a seek bar, a **speed** button (0.75×–2×, remembered), and
**Text** / **Images** toggles that show or hide the article text as karaoke (the whole article scrolls, the sentence being read is marked and the current word is highlighted; word timing is estimated from the MP3 position) and the article's current picture. Everything is a
large touch target and keyboard reachable; the speed also applies to the
playlist player.
**Hands-free:** while a track plays, the lock screen, notification shade, Bluetooth headset and car display show the article title, magazine and first-page artwork and offer play/pause, previous/next article and ±15 s (and seeking, where the device offers it). In driving mode the screen is kept awake while audio plays and allowed to sleep when it is paused or you leave. Both are optional browser features; where one is missing or refused, playback itself is unaffected. Opening driving mode resumes the first unfinished article at the position you left it.
**Layout and views:** driving mode is a top bar, a content area and a controls dock. The dock (position slider, time and playback speed, then previous, back 15 s, play/pause, forward 15 s, next) is always completely visible at the bottom and is never covered or pushed off screen, on phones from 320x568, tablets and in landscape. The top bar has *Share*, a *menu* button that shows the current view, *Index* and *Exit*. The menu opens a flyout in four groups. **View** (radio options with icon and name): **Picture** (the whole picture, no text), **Text** (the whole text, lyrics-style, no picture) and **Split** (picture plus three lines, the default; in landscape side by side). **Show** (switches, label left and switch right; on = thumb right with a tick, off = thumb left with a cross): *Show text* and *Show pictures* hide just that part in the views that contain it (so with Split you can drop either), and *Minimize text and picture*. **Playback:** *Play next article automatically* (see below). **Theme:** one control with *Light*, *Auto* and *Dark*; the chosen one carries a tick. Each control is at least 44 px high and the flyout fits phones from 320 px wide. Keyboard: Enter or Arrow Down opens the menu; Up/Down/Left/Right move over every control, Home/End jump to the first/last, Space or Enter switches or selects, Escape or a tap outside closes it and returns focus to the menu button. Choosing a view closes the flyout; switches and the theme keep it open so you see the effect. **Automatic next article:** when an article ends it is marked finished and the next article that has audio starts (articles without audio are skipped, the speed stays); title, picture, text and the index follow and a screen reader hears "Now playing: ..." once. After the last article it stops and shows *End of magazine* (it never starts over). Switch *Play next article automatically* off to stop after each article. An article whose audio cannot be loaded is skipped with a short message. Older saved views are converted (Lyrics to Text, Compact to Split). **Index:** the *Index* button opens a list of all articles of the magazine with their length (articles without audio are dimmed and marked *no audio*). The article that is playing is highlighted; tap it or press Enter on another to jump there while driving mode keeps playing. Every row is its own item, separated from the next by a thin divider line and generous space above and below (the playing row is highlighted instead), so long two- or three-line titles do not run into each other. Escape or the close button closes the list (Tab stays inside it while open); the controls below stay visible and usable by touch. Lengths come from the already generated audio, nothing is recomputed. Each row also has a round **play button** (Play for unplayed, *Resume* with a circular-arrow icon for partly played, *Play again* with the replay icon for finished articles; it starts that article in driving mode and continues from the saved position), the author as *by Name* in smaller muted text under the title (omitted when unknown, long names are shortened), and a status icon on the right (empty circle: not played, dashed circle: partly played, check: finished) that also has the text *Not played / Partly played / Finished-Played* for screen readers and updates live while you listen. **Speed** is the button after the time next to the slider. **Picture zoom:** double-tap or double-click, pinch, mouse wheel, the + and - keys, or the zoom in / zoom out / reset buttons on the picture; drag (or arrow keys) to move a zoomed picture; Escape or the reset button returns to full view. **Lyrics:** the spoken sentence is large and kept in the middle, earlier and later ones are dimmed; tap or press Enter on a sentence to play from it; scrolling by hand stops the automatic following and shows *Back to current*. With reduced motion switched on in your device the text jumps instead of gliding. **Minimize** (in the menu) reduces the area to the title and the current line so the dock keeps minimal text; choose it again to expand. Your view and the minimized state are remembered in this browser only. Titles wrap to two lines (with an ellipsis beyond that), picture captions to two lines. **Theme (night driving):** the *Theme* control at the end of the menu offers **Auto** (follows the system, default), **Light** and **Dark** (night). Dark is a night palette for the whole driving overlay (top bar, menu, index and rows, text lines, slider, dock, zoom buttons, the *Tap to play* button, toasts and the share box): true black background, text at about 85% white, a muted blue accent, pictures slightly dimmed, and the browser's address bar colour follows while driving mode is open and is restored when you leave. Text keeps at least 4.5:1 contrast and buttons/borders 3:1, also for rows without audio (shown dimmed with a dashed play button); the listening states stay distinguishable by icon, not only by colour, and the focus ring and spoken-word highlight stay clearly visible. Auto switches live when your phone changes between day and night mode. The choice is independent of the rest of the app and remembered in this browser only (if storage is blocked it applies until you leave the page). Nothing glides or fades in extra for the theme, so reduced-motion settings are unaffected.
**How much text (Split view):** the menu section *Text in Split view* sets the amount, remembered on this device. **Sentence** (default) shows only the sentence being read, large; **Context** shows the previous sentence (small), the current one (large) and the next (small); **Paragraph** shows the whole paragraph the sentence belongs to with the current sentence bold and underlined and the spoken word marked. The text frame always stays between the picture and the controls: the current text shrinks from 1.5 rem down to 1 rem (it follows your device's text size and browser zoom) until it fits, the picture gives way before the text is squeezed, and whatever still does not fit scrolls inside its frame while following the spoken word (scroll by hand to look around; it follows again at the next sentence). In landscape the picture and the text sit side by side. The *Text* view always shows the whole text. Tap or press Enter on a dimmed line (Context, Paragraph) to jump to it; the text is not announced sentence by sentence.
**Context lines:** with *Context*, driving mode shows the previous sentence (small), the current one (large, the spoken word marked) and the next (small) in a box of fixed height, so nothing moves around it. Tap or press Enter on the previous or next line to jump to that sentence. The text is deliberately not announced by screen readers sentence by sentence; the play/pause button and the position slider stay the way to control it. With reduced motion switched on in your device, lines change without animation. On a phone (375 px wide) the sentences wrap and a very long current sentence scrolls inside its box. Turn the text off with *Show text* in the menu to see only the picture. **Lyrics** in the older wording is now the **Text** view.
**Text and picture follow the voice:** for audio generated since sentence times were recorded, the highlighted sentence (playlist and driving mode) and the picture shown come from the exact time each sentence was spoken, so they stay right after the slider, the 15-second buttons, a tap on a sentence, the lock screen or a resume. Captions are shown but are not in the MP3, so they are never highlighted. The highlight lights up a sentence just before its first word is heard (each sentence starts with a short silence, which is not counted). Audio from before sentence times were recorded, and audio whose text was edited afterwards, has no usable record and is followed by an estimate from sentence length (less exact); regenerate the audio to get exact sync. Interview-voice audio is covered like any other. In interviews, the interviewer (`c't:`, `Redaktion:`, `Interviewer:`, `Frage:`) is read in the narrator voice and each guest in the second voice; paragraphs without a label continue the previous speaker, and the labels themselves are not spoken.
**Word by word:** the first time an article is played, the app asks the forced-aligner service (the separate `tts-aligner` container, see below) to measure where each word starts and ends and stores that beside the audio. Playback never waits for it: until it is ready (usually well under a minute for a magazine article) the marked word is estimated from word length as before, and it switches to the measured word on its own. The marked word is the last one that has started, so it stays lit through a pause; the *Lyrics* view now marks the spoken word too (as do *Compact* and the playlist text). If the service is not running, nothing changes for you and the app tries again later (at most every 10 minutes per article). Prerequisites: `docker compose up -d tts-aligner` (first start downloads about 1.2 GB of model weights into the `aligner-models` volume; after that it needs no internet). `ALIGNER_URL` empty turns word timing off. Privacy: text and audio go only to your own container on the internal Docker network. Accessibility: the marking is visual only and adds no announcements; with reduced motion nothing animates. API: `POST /api/magazine/{doc}/articles/{id}/align-words` starts it explicitly, `GET` shows progress.
**Share a link:** the *Share* button in the toolbar (the whole magazine), the share button in the playlist header and the one at the top left of driving mode (the current article) share a link of the form `<this app's address>/#s-magazine?doc=<magazine>&article=<article>&mode=drive`. Where the browser has a share sheet (phones, and any browser on https) it opens; otherwise the link is copied and a confirmation is announced (on a plain-http address a box with the link is shown to copy by hand). Opening the link goes straight to that article in driving mode with audio, pictures and text; if the browser refuses to start audio without a tap, a large *Tap to play* button appears. A link for the whole magazine opens at its first unfinished article. **Privacy and access:** the link contains only ids, no password or key. It is just an address: whoever opens it needs to reach this app the same way as for any other page (this app has no user accounts; access is controlled by your network or reverse proxy), and an unknown or removed article simply shows a message. Only articles that already have audio can be shared. A link whose parameters are not in the expected form is ignored.
**Expected result:** the article plays from its headline to its final paragraph.
Page furniture (running heads, page numbers), adverts, sidebars and pull quotes
are never read aloud unless you explicitly include them in review.
### Confidence, and what it means
| Shown | Meaning |
|---|---|
| ~90% | The contents page listed this article — title and start page are known. |
| ~55% | Detected from a headline alone; check the boundaries carefully. |
| Low | No body copy found. Usually an advert or a section divider — reject it. |
An article is never narrated until you approve it, whatever its confidence.
### Podcast mode
Choose two or three voices as hosts. The configured LLM writes a discussion of
that article for listeners who have not read it, and each host's turns are
synthesised with their voice.
The prompt forbids inventing facts, figures or quotes that are not in the
article; if the article reaches no conclusion the hosts say so. It is still a
generated discussion, not the author's words — treat it as a summary, and read
the original for anything that matters.
Podcast generation is refused for an article you have not approved, because
unreviewed boundaries may include a neighbouring article or an advert.
### What is wrong on this page, and fixing it automatically
**Purpose:** see at a glance what is wrong with a page and fix it without hunting for the right button.
**Steps:** open a page in review. The **What is wrong on this page** panel lists each problem in plain language and marks it on the page: **red dashed frame** = printed text outside any box (never read); **orange edge** = a box edge cutting through text; **purple dashed outline** = two boxes over the same text; **yellow dotted outline** = words that look mis-read; **red wavy underline** (in the list) = words no dictionary knows. Each row has its own button (*Find missed text*, *Fit boxes to the text*, *Verify OCR text*, *Fix with language model*). **Fix this page automatically** does all of them in one go: fits boxes, adds an inset pull quote, cuts a paragraph that wraps around it, finds missed text, re-reads suspicious words and, when a language model is configured, fixes provable misspellings.
**Result:** the page reloads with what was fixed; **Undo** (above the list) restores the page as it was. Overlapping boxes have no automatic fix: delete or merge one.
**Where an article ends:** the last box says *article ends* only when the article is signed off (`(rah) et`) or a headline/deck/byline follows it (same page or top of the next). Otherwise it shows *no end marker — link page N ›*; click it to add the text of that page that carries the article on (Undo reverses it).
**Reading order:** applied automatically after import, *OCR again* and *Fix this page automatically*; pages whose order you set by hand keep it (*Automatic order* releases a page).
**Text flow:** green arrows and chips on the page show where the article carries on: an arrow to the next box below, a curved *next column* arrow, and a chip *continues on page N ›* / *‹ from page N*, plus *article starts / ends*. Tick or untick **Show text flow** to hide them. In the reading view a small marker (`↓`, `col ›`, `p.N ›`) sits at the word where a paragraph crosses a box, column or page. The markers are labelled for screen readers.
**Whole magazine:** a fresh import now runs this automatically as its last step -- the magazine you open is already as fixed as the machine can make it. **Auto-fix all pages** (contents view, formerly *Quality check / run*) repeats it on demand (for instance after a manual box edit) and lists the pages that still need a look (purple badges).
**Errors:** a failed fix shows the reason and changes nothing. **Offline / privacy:** the box fixes and the audit are local; the OCR re-read and spelling steps use only the engines you configured.
**Colours in the article list:** a **blue** row with a *not reviewed* pill is an article nobody has approved or rejected yet; a green edge means approved.
### Undo and Redo
**Purpose:** take back a change (or put it back) without leaving review.
**Steps:** use **Undo** / **Redo** in the sticky bar (at the left of its second row), or Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y). The button tooltip names the change, e.g. "Undo: Deleted 2 regions". The last 20 recorded changes are kept per magazine: region edits and moves, drawing a region, approving/rejecting an article, page linking, auto-fix, reading-order changes, box clean-up and spelling corrections.
**Errors:** a button stays disabled when there is nothing to undo/redo, or when something else has changed the magazine since (for example a whole-magazine run), because undoing then would roll that work back too. A new change ends the redo history.
**Privacy:** the saved states live in the magazine's own `undo/` folder; nothing is sent anywhere.
### Less manual work: what happens automatically
At import (as its automatic last step, not a separate click any more), and whenever you press **Auto-fix all pages**: boxes are fitted, inset pull quotes found, wrapped paragraphs cut, continuation pages **linked into the article running from the page before**, missed text added, suspicious words re-read and (with a language model) provable misspellings fixed. The report then lists only the pages that need a *decision about their boxes*, with **Start with page N**; pages that only have words to check are collapsed and optional. Articles that gained pages go back to *detected*, so re-approve them (the batch tools in *Select* do many at once). A backup of the layout is kept. The ingest status bar keeps showing progress ("Checking page N of M: …") through this step, so a fresh import simply takes longer before it says done -- an Auto-fix failure (for example an unreachable OCR engine) is reported as an ingest failure rather than a silent gap, but nothing already imported is lost.
**When the automatic link misses:** open the article in review and use **Link to page N** in the panel: *Article continues there* adds the text that belongs on that page (from the paragraph that carries the sentence on, up to the next headline), *Article starts there* adds the opening. Added text is ticked, so untick what does not belong. **Undo** reverses it.
### Clean up boxes: fitting boxes to the text
**Clean up boxes** (review toolbar) also fits each text box to the ink of its text: a side that cuts through letters grows a little, empty margin is trimmed. Boxes you drew or resized are never changed; a backup of the layout is kept and the message reports how many boxes were fitted.
**Inset pull quotes:** a quote set into the text in a display colour is added as its own *pull quote* region (unticked, never read aloud) by **OCR again (this page)** and **Clean up boxes**. If a picture or infographic is picked up by mistake, delete the region or change its type.
### One paragraph, one block — even across columns and pages
**Purpose:** a paragraph that continues into the next column or onto a later page is separate regions in the layout. Reading and the MP3 join them before splitting into sentences, so narration never stops mid-sentence at a seam.
**Prerequisites:** none — this happens automatically for every approved article.
**How it works:** consecutive body regions are joined when the first one does not end a sentence and the next one continues it (lower-case start, trailing hyphen or comma, or ending on an article/preposition/conjunction). A word split at the seam is closed up (`jähr-` + `lich` → `jährlich`); a real compound hyphen is kept (`KI-` + `Modelle`). Headings, interview turns and finished paragraphs are never glued together, and a caption or figure description between two fragments is moved after the paragraph.
**Result:** the reading view and the exported MP3 speak the fused paragraph as one block; highlighting and "Original page" still follow the region each sentence started in.
**Errors:** if two paragraphs are wrongly joined or a split is missed, correct the region text or reading order in review; the text is recomputed each time.
**Mobile / accessibility / offline / privacy:** no extra controls; nothing leaves the machine.
### Pronunciation & spelling (per article)
Open an article and expand **Pronunciation & spelling** under the player.
**Acronyms — purpose:** say `NAS`, `ML` or `SSD` the way a listener expects, in the article's language. The language is decided once for the whole article (a short fragment alone is a poor signal); an English quotation inside a German article is spoken with English expansions.
**Steps:** unfamiliar acronyms are listed with the sentence they appear in. Type how the narrator should say one (empty = as printed) — the change applies immediately and stays with the article — or press **Let the language model decide**: it picks *expansion*, *letter by letter* or *as a word* from the sentence's context and, for English acronyms in a German article, prefers the established German term. Only expansions carry model-written words; letters/word choices are decided by the server.
**Known acronyms:** a built-in list covers common German and English ones (`KI`, `AI`, `LLM`, `API`, `GPU`, `USB` → "U S B", `HDMI`, `z. B.` …). The displayed text is never changed.
**Spelling — purpose:** find OCR and typing errors in German and English text without ever rewriting names or technical terms by guess.
**What is flagged:** a word unknown in the text's language, not a known compound (`Softwareentwicklung`), not known in the *other* language (an English term inside a German article is fine), not an acronym, not a name in mid-sentence English, and not a word you kept. A word that recurs three or more times is treated as vocabulary.
**Marked on the page:** in review, flagged words are underlined in the region list and the **Spelling** button lists them for the page with suggestions. In the reading view the same list is under **Pronunciation & spelling**. Click a suggestion to correct the word in the region text (review offers **Undo**), or **Keep** to add it to this issue's dictionary.
**Fixed by an agent:** **Fix with language model** (page or article) asks the configured LLM about each flagged word with its sentence. A correction is applied only if it is one of the dictionary's suggestions or a single known word within two edits of the printed one; names and terms the model keeps go into the issue's dictionary, everything else stays marked. The whole-magazine **Quality check / run** does the same for every page (when an LLM is configured) and lists pages that still have marked words.
**Errors:** "No LLM model configured" — set one in Settings; the marked list still works without it. A stale suggestion ("the text has changed") — reload the list.
**Mobile / accessibility:** the lists are ordinary buttons with labels ("Replace X with Y"); marks are underlined, not colour-only.
**Privacy:** the agent sends only the flagged word and its surrounding sentence to the LLM you configured in Settings; without an LLM nothing leaves the machine.
### The page overview: what every page needs from you
**Purpose.** The page grid (the *Thumbnails* view of a magazine's contents) shows every page of the issue at once and tells you,
in words, where you still have to act.
**Steps.** Read the line above the grid: "*N of M pages need you*". Tick **Show only pages that need me** to hide everything the
system has settled. Each card shows, top to bottom: the page, **how many articles are on it**, each article's title with
**"continues from p.X" / "continues on p.Y"** where it flows to another page, a **status** (for example *Text found, not checked yet*,
*Problems found*, *Approved*, *Done: audio is ready*, *Advertisement: not read*), and either a blue **To do** box or "Nothing to do
here". Click the page image to open it in Review.
**Filter by status.** Every entry of the legend (*not reviewed yet*, *has content, not quality-checked*, *quality check clean*, *quality check found
problems*, *reviewed*, *advertisement*, *reviewed + audio generated*, *reviewed, nothing to narrate*) is a button showing how many pages have that status. Press one
to show only those pages; press several to show pages that have **any** of them; press one again to take it off. A status no page has is greyed out.
It combines with **Show only pages that need me** (a page must match both), the summary says "Showing N of M pages", and **Show all pages** clears every
filter at once. The buttons are real toggle buttons (they announce whether they are on) and can be used from the keyboard.
**What the to-do lines mean.** *Open the page … and approve it*: detection found text and nobody has confirmed it. *Problems found*
lists what the quality check saw and which tool fixes it (*Find missed text*, *Clean up boxes*, *Spelling*, merge or shrink
overlapping boxes, keep a page in one article only). *Text was found here but it belongs to no article*: link the page to the
article it continues, or start a new one. *Generate the audio*: approved, but no MP3 yet. *N of the M articles here still wait for
approval*: a page shared by several articles is coloured by its most advanced one, so check the others. A small grey note tells you
when a page you approved had problems that the quality check found earlier.
**Expected result.** When the summary says nothing is left, every page is approved, settled as an advertisement or empty, or has audio.
**Errors / limits.** The quality-check lines appear only after a quality run (*Auto-fix all pages* or *Quality check*); before that,
pages show the status only. A page with no text and no article is reported as having nothing to do, which is right for a full-page
picture but worth a glance if you expected text.
**Mobile / accessibility.** Status and to-do are plain text beside the colour, so nothing depends on seeing colours; the legend is 14 px
with large swatches. On a phone use fewer columns (the *Columns* slider) for more readable cards.
### EPUB with synchronised audio
**Purpose.** Take an issue to an e-reader or reading app that highlights the sentence being spoken (EPUB 3 *Media Overlays*).
**Prerequisites.** At least one approved article with generated audio. Only those are included, in contents order.
**Steps.** Open the magazine's contents and press **EPUB with audio**. The book downloads; open it in a reader that supports
read-aloud overlays (for example Thorium Reader, Apple Books, or Readium-based apps).
**Expected result.** One chapter per article, a table of contents, the MP3s inside the file, and each sentence linked to its
place in the audio. The file passes the W3C EPUBCheck (0 errors, 0 warnings on a real two-article, twelve-minute test).
**How exact is the highlighting.** Audio rendered **after 2026-09-30** (including interview-voice renders) records where every sentence starts and ends, so the
highlight is exact. Older audio has no such record: its sentences are placed by estimate, proportionally to their length
across the real duration, so the highlight can drift by a sentence or two. The book's description states which it is; to get
exact timings for an older article, regenerate its MP3.
**Errors.** No approved article with audio gives "No approved article has audio yet". Audio whose length cannot be read is
skipped. Sidebars and figures are not part of the book.
**Mobile / accessibility.** The book is reflowable text, so text size and contrast are set in the reader. A screen reader
reads the text directly; the overlay is an addition, not a replacement.
**Offline.** The file is self-contained. **Privacy.** It contains the magazine's text and your generated audio, so share it only
where the magazine's licence allows.
### A page that was scanned sideways or upside down
**Purpose.** Some scans arrive turned: the page looks sideways, and what OCR read from it is gibberish.
**Prerequisites.** A vision OCR engine set up in Settings (the same one *OCR again* uses), because turning re-reads the page.
**Steps.** Open the page in Review and press **Turn right** (90° clockwise), **Turn left** (90° counter-clockwise) or **Turn 180°**.
The app asks first, then turns the page image, reads the page again, and shows the result. Tick **two-page spread** to choose which
of the two pages the buttons act on, as with the other page tools.
**Expected result.** The page stands upright, its boxes and text are new readings of the upright page, and the page's quality findings
are re-measured. On a test page, reading it sideways scored word-F1 0.02 and reading it after turning 0.88 (an ordinary page scores
about 0.88). The turn is remembered: re-running detection or re-importing does not flip the page back.
**While a turn is being read** the picture and text you see are the old ones: the notice above the page says so, shows the time spent, and
dims them until the read finishes (usually 30 to 60 seconds). The text is never turned itself; it is read again from the turned picture.
**Keeping track of turns.** Turns add up: a right turn plus a half turn is three quarters. The toolbar says so in words (for example
"Turned 270° clockwise from the scan") and offers **Undo turns**, which returns the page to the orientation the PDF itself gives it. Only one
turn or re-read of a page can run at a time: pressing a button again while one is still working is refused with a message, instead of stacking
a second turn. The turned picture appears as soon as the page is re-read; page pictures are requested under a version number, so a browser
that remembered the old picture fetches the new one. Note what a PDF's own orientation can be: some pages are stored sideways on purpose
(c't page 156 is a landscape page that reads sideways), and one **Turn right** is then the whole fix.
**What is replaced.** The page's boxes and text are replaced by the new reading, so corrections you made to that page by hand are lost, and
articles that used the page go back to "not approved" where the new boxes do not line up with the old ones. **Undo does not cover a turn**
(it changes the page image); if you turned the wrong way, press the opposite button.
**How the page is read after a turn.** An OCR engine can read a page well in one orientation and return nothing in another (on a real
page it read the sideways image at word-F1 0.88 and gave an empty answer for the same page turned upright). So after a turn the app tries, in
order: the turned page with your vision engines; then the page as it was scanned, with the boxes carried over into the turned page; then tesseract
on the turned page. The first plausible reading wins, and the finished message says which was used. A picture page with no text can be turned too.
**Errors.** Without a vision OCR engine nothing is changed and you are told why. If neither the engines nor tesseract can read the page, the page image is
put back exactly as it was and nothing is saved. A page that has a real text layer in the PDF is read from its image after a turn, because the
PDF's own text positions belong to the unturned page.
**Mobile / accessibility.** The three buttons are real buttons in a labelled group, each with its direction in words, and the confirmation is a dialog you can answer with the keyboard.
**Privacy.** The page image goes to the OCR engine you configured, exactly as for *OCR again*.
### Editing recognised text, and seeing what you changed
**Purpose.** Fix what OCR got wrong and always be able to see, and undo, what you changed.
**Fix a flagged word where it stands.** Words the spelling check doubts are underlined in the region's text. Click one (or Tab to it
and press Enter): only that word turns into a small field, in the same type as the paragraph, so nothing around it moves or
changes. Type the correction and press **Enter**, or click one of the suggested words; **Esc** puts the word back; **Keep**
stops flagging a name or technical term in this issue. Each fix can be undone with Undo, like every other correction.
**Steps.** In Review, click a region's text away from an underlined word (or its pencil) to edit the whole text in place: it keeps the paragraph's type, width and line breaks, grows with what you type, and the doubtful words stay underlined (an underline goes as soon as the word is changed). Press *Save text*. A region you edited shows an
**Edited** mark. *Show changes* opens a word-by-word comparison with the text as first recognised: words removed are
struck through and marked with a minus, words added are underlined and marked with a plus (never colour alone).
*Restore recognised text* puts the original back. Typing the original back yourself also clears the mark.
**Expected result.** The first recognised text is kept through any number of edits; reading the region again (*Re-OCR*)
makes the new reading the original. Edits are reversible with Undo as before.
**Errors.** Empty text is refused (delete the region instead). Very long texts (over about 1,500 words) are compared as
one removed block and one added block rather than word by word.
**Mobile / accessibility.** The buttons are real buttons with an expanded/collapsed state; the comparison is a labelled
group that explains the minus/plus convention to a screen reader.
**Privacy.** The recognised text stays on the server with the magazine; nothing is sent elsewhere.
### Find an issue online
**Purpose.** Pull a magazine issue into the app without leaving for a browser and uploading by hand.
**Prerequisites.** Internet access for the Internet Archive. For your own library, an OPDS catalogue (Calibre-Web, Kavita or
Komga all serve one): open **Your catalogues (OPDS)** in the same panel and add its name and address (for example
`http://nas.local:8083/opds/`). If it needs a login, enter the username and the **name of an environment variable** that holds the
password (for example `NAS_OPDS_PASSWORD`), and set that variable for the app's container; the password itself is never typed
into, stored in, or shown by the app, and it is sent only to that catalogue's own address. The list tells you if the variable is not
set on the server. Up to 10 catalogues. Use an `https://` address for a catalogue reached over the internet: a login to an `http://`
address travels unencrypted.
**Steps.**
1. Open Magazines. In *Find an issue online* pick a source, type a title or subject, press Search.
2. Read the licence line on each result. The source states it, the app does not judge it.
3. Press *Import* on a result. The PDF downloads (progress is announced under the search box), the magazine
opens and ingestion starts, exactly as after an upload.
**Expected result.** A new magazine project with the PDF stored locally, then the normal review-before-narration flow.
**Errors.** A source that is down, rate-limiting or returns something other than a PDF gives a plain message
under the search box and nothing is created that you must clean up beyond the empty project shown in your library.
Lending-only Internet Archive items are never listed. Only one download runs at a time; a PDF larger than
400 MB is refused. A catalogue's results are read across up to five of its result pages, so "next page" of a large library works.
**Mobile / accessibility.** The form stacks on a phone; results are a list with labelled Import buttons; status
text is a polite live region, so a screen reader hears "N results" and download progress.
**Offline.** Search and import need the network; everything after import works offline.
**Privacy.** Your search words go to the chosen source (archive.org, or your own catalogue). Nothing else is
sent. Websites that redistribute commercial magazines without a licence are deliberately not supported.
### Quality check / run (whole magazine)
**Purpose:** find every page where text is not inside a box, boxes overlap or cut off letters, or OCR still looks wrong.
**Prerequisites:** the magazine has been ingested. A vision OCR engine (Settings) is optional; without it the word re-reading step is skipped and the result says so.
**Steps:** on the contents screen press **"Quality check / run"** (first button in the toolbar) and confirm. Progress shows "Checking page N of M".
**Result:** the run (1) adds text regions that independent OCR found outside every box (unticked, so nothing new is read aloud until you approve it), (2) re-reads suspicious words from the page image and only accepts a clearly better reading, (3) measures the page image for ink outside every box, overlapping text boxes, boxes whose edge (left, right, top or bottom) cuts through text, and articles that claim the same regions (the same text would be read twice). Pages that still need a look are listed as buttons under the toolbar; each opens that page in review.
**Errors:** one page failing does not stop the run; it is listed as "check failed". The report is kept in the magazine's folder (`quality_report.json`) and served at `GET /api/magazine/{id}/quality/report`.
**Page colours and live indicator:** in the thumbnail grid, pages the run found clean turn blue and pages with problems turn purple, whatever their review state (only a page whose every article was rejected keeps its grey). A purple warning badge with the problem count sits on the thumbnail; hover or focus it to read what was found, click it to open the page in review. The colour reflects the last run — run the check again after fixing pages. While the run is going, the page being worked on pulses and names its step (finding missed text / re-reading suspicious words / measuring box coverage), and finished pages show "✓ checked". Pulsing is disabled under reduced-motion. The same live indicator works for every batch step while the thumbnail grid is showing: reading pages (OCR), finding advertisements / recognising paragraphs during detection, fixing all figure boundaries, and rendering audio (the pages of the article being rendered pulse with "rendering audio n/total").
**Advertisement pages:** a page where detection found advertisement regions and no article text turns green with a dashed border on its own — nothing on it is read aloud and the quality check skips it. No approval step is needed; a page that mixes an ad with article text is treated as an ordinary page.
**Mobile / accessibility:** the list is a labelled live region of ordinary buttons, keyboard reachable.
**Offline / privacy:** the missed-text pass uses local OCR; the word re-read uses only the vision engine you configured. Nothing else leaves the machine.
### Untitled entries ("Page 33") and continuation pages
**Purpose:** clean up contents/driving-mode entries that have no real title (`Page 33`, `Page 69` …). Most of them are continuation pages of the article before them that ended up as entries of their own. The others are advert pages, picture pages, or short text fragments.
**Why they exist:** the review screen creates such an entry when you open a page no article covers. A page loses its article when an article is approved on another page without any box being toggled: only the visible page's boxes were saved, so the other pages fell out of the article.
**What you get (nothing changes on its own):**
- Every entry now carries a **display title**. Real titles are shown unchanged. Placeholder titles show a suggestion instead: *Anzeige*, *Bildseite*, *Inhalt*, *Impressum*, *Editorial*, *Textfragment*, the page's own headline, or its first sentence (at most 8 words). The page number comes separately (`page_label`, e.g. "Seite 33") so the index can show it in smaller text.
- A **merge proposal** when an entry looks like the continuation of the previous article. Each proposal has a confidence (*high* ≥ 0.75, *medium* ≥ 0.5) and the evidence, for example "starts lower-case / mid-sentence", "previous text ends in a hyphenated word break", "adjacent page" or "previous article is signed off".
- `GET /api/magazine/{id}/untitled` lists all of them with counts. The quality report (`GET …/quality/report`) carries the same list as `untitled_articles`.
**Steps:**
1. Look at the proposals (`GET …/untitled`, or the dry run below).
2. Merge one: `POST /api/magazine/{id}/articles/{article}/merge-into-previous` with `{"into": "<proposed id>"}`. It can be undone with **Undo**.
3. Accept a title: `POST …/articles/{article}/accept-title` (optionally `{"title": "…"}`). An entry that already has a real title is refused, so titles you typed are never overwritten.
4. Optional, better titles from the local LLM: `POST …/titles/suggest` (background job). It only fills the suggestion cache (`title_suggestions.json`). Switch it off with the setting `magazine_llm_titles: false`.
5. Whole magazine from the command line: `python3 scripts/magazine_untitled.py <doc-id>` is a dry run that changes nothing. Add `--apply` to write: a backup `articles.backup-untitled-<time>.json` comes first, then every proposal ≥ `--min-confidence` (default 0.75) is merged in page order and suggested titles go into placeholder titles. `--no-llm` keeps it fully deterministic.
**Expected result:** continuation pages become part of their article (pages and regions in reading order), and the index shows readable names with the page number beside them.
**After a merge:** the merged article is marked `audio_stale` ("render the audio again"). Its old MP3 still plays the shorter text until you render it again, and a fresh render clears the mark. The merged-away entry's MP3 and timings stay on disk and are not deleted. An approved article stays approved only if both parts were approved.
**Errors:** "The proposal changed" (409) means the articles changed since the list was loaded, so reload and look again. "No merge is proposed" (409) means the entry is not a continuation by the current rules. If the LLM is unreachable, the run says so once and uses the deterministic titles.
**Mobile / accessibility:** data only (API fields). The index shows `display_title` with `page_label` in smaller text, and the full title stays available to screen readers.
**Offline / privacy:** the heuristics run locally. LLM titles use only the LLM configured in Settings and send it at most 1,500 characters of the entry's text.
### Marking exported audio as synthetic speech
- **Purpose:** exported MP3s (single download, "Download all (ZIP)", "EPUB with audio") carry tags saying the speech is AI-generated.
- **Prerequisites:** an article with generated audio. No extra software.
- **Steps:** in the export row tick or untick "Mark as synthetic speech (tags)" (default on, remembered as a setting) and optionally "include voice name". Then download as usual.
- **Result:** the copy you download has tags `synthetic_speech`, `generator`, `generator_version`, `generated_date` (and `voice` if chosen). The stored audio is unchanged. To check a file: `python -m core.export_provenance file.mp3` or `POST /api/export/provenance` with the file as body.
- **Errors:** if tagging fails the download answers "Could not add provenance tags"; untick the box to export untagged.
- **Mobile / accessibility:** plain labelled checkboxes. **Offline:** works offline. **Privacy:** no user names or paths; the voice name only if you tick it. Tags can be removed by anyone, so they are an honest label, not protection. An inaudible watermark and a spoken disclosure are not available yet.
### Errors
| What you see | What happened | What to do |
|---|---|---|
| "Upload the PDF first" | The project has no `source.pdf`. | Upload it, then ingest. |
| Ingestion failed | The PDF is encrypted or corrupt. | Check it opens in a viewer. |
| "Approve the article before generating a podcast" | Review gate. | Approve it first. |
| "No LLM model configured" | `llm_model` is empty. | Set it in Settings. |
| "LLM did not return usable JSON" | The model ignored the format. | Retry, or pick a stronger model. |
| "Article has too little text" | Under 200 characters — usually a stub. | Check the boundaries in review. |
| A figure box sits over the wrong part of the page | The OCR engine grounded the figure loosely. | Press **"Fix figure boundaries"**; if the graphic is too pale to detect, draw the region by hand. |
| "Fix figure boundaries" reports nothing corrected | Every box already matched its page, or none could be placed with confidence. | Nothing is wrong; correct any remaining box by drawing it. |
| "No vision OCR engine configured" (Describe) | `vision_ocr_url`/`vision_ocr_model` are empty. | Set a vision engine in Settings. |
| "The vision engine returned nothing usable for this figure" | The configured engine (and fallback, if set) returned an empty or degenerate response for this image. Some vision models are transcription-only and cannot produce open-ended descriptions at all, however the prompt is phrased — confirmed with a pure-OCR model that answers its own "Text Recognition:" prompt but returns nothing for any descriptive one. | Configure a genuinely caption-capable vision model as the fallback (`vision_ocr_fallback_url`/`vision_ocr_fallback_model`), or write the description by hand. |
| "Save & re-OCR" on an edited box leaves the old text in place | The corrected area produced an empty or unreadable OCR result — the same engine limitation that can affect figure descriptions applies here too. | Press **Save** to keep the corrected box position with the existing text, then correct the text by hand if needed, or try "OCR again (this page)" once the box position is saved. |
### Privacy
Ingestion, OCR and storage are entirely local to the container. Podcast mode is
one exception: **the article's text is sent to whatever LLM `llm_url` points
at.** Generating a figure description is the other: **the cropped image is
sent to whatever `vision_ocr_url` (and, on failure, `vision_ocr_fallback_url`)
points at.** If either is a hosted API rather than a model on your own
network, that content leaves your machine. Narration uses your local TTS
stack and sends nothing outward.
### Accessibility and mobile
Sentence and word highlighting both respect `prefers-reduced-motion` (no
smooth-scroll animation when it is set; the highlight itself is a colour
change, not motion). Driving mode gives large, high-contrast controls
(skip, ±15 s, play/pause, speed) — hide Text and Images for a bare screen — for use without
looking at the screen — it does not, and should not, replace watching the
road. Word-level highlighting can be switched off in the reading controls if
it is more distracting than helpful; sentence highlighting keeps working
either way. Keyboard navigation of the contents tree and review screen is
not yet built.
### Offline
Ingested magazines are stored server-side, so any device on the network sees the
same library and reading progress. Playback needs the server; there is no
offline cache yet.