Add REST API server, standalone web interface, and missing pieces - #368
Open
Eddyosas008 wants to merge 86 commits into
Open
Add REST API server, standalone web interface, and missing pieces#368Eddyosas008 wants to merge 86 commits into
Eddyosas008 wants to merge 86 commits into
Conversation
- server.py: FastAPI REST API with /api/tts (voice design, controllable cloning, ultimate cloning), OpenAI-compatible /v1/audio/speech endpoint, /api/health, thread-safe lazy model loading, and wav/flac/ogg output - web/index.html: standalone French/English web interface (VoxCPM Studio) with the three generation modes, advanced settings (CFG, diffusion steps, seed), drag-and-drop reference audio, session history, and WAV download - app.py: make the funasr import optional so the Gradio demo starts without it (ASR auto-transcription degrades gracefully with a clear message) - examples/input.txt: add the batch-mode sample file referenced by README - pyproject.toml: add [server] optional dependency group - Dockerfile: GPU-ready image serving the API and web UI on port 8000 - docs/GUIDE_FR.md: full usage guide in French (web UI, REST API, CLI, Python API, Docker, troubleshooting) - README.md: document the new web interface and REST API - tests/test_server.py: 13 endpoint tests using a stub engine (no torch or model weights required) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MeWfqpDWJs9veHKHRzQqG9
…reviews - Add PRESET_VOICES dropdown (7 French narration voices) that auto-fills the description, seed (editable, random-seed unchecked), CFG, diffusion steps and text-normalization. "Personnalisé / manuel" keeps the current free behavior. - Load voices from editable conf/preset_voices.json (falls back to built-in list). - Add full French i18n locale (fr) alongside en / zh-CN. - Long-text chunking for audiobooks: split on sentence boundaries, synthesize each segment with the same seed for a consistent voice, stitch with short silences, with a per-segment progress bar. Toggle in Advanced Settings. - On-demand voice preview button caching wavs to assets/voice_previews/. - Import a .txt chapter into the target text box (UploadButton). - Add --no-denoiser flag: skip the ZipEnhancer (ModelScope) denoiser, only needed for reference-audio cloning; avoids a slow/blocking download on CPU-only setups. - Add scripts/pregenerate_previews.py to warm the preview cache offline. - Add examples/texts/chapitre_exemple.txt and gitignore the preview cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYwW5KLZYovWstkx9aCwiq
…ename Each generation is now written to output/ as narration_<voice>_seed<seed>_<timestamp>.wav (voice = preset name or "custom") and the player serves that file, so the download button keeps a meaningful name. The output/ directory is gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYwW5KLZYovWstkx9aCwiq
Add a "Caractères max par segment" slider (100-600, default 300) in Advanced Settings so the long-text chunk size is tunable from the UI instead of being hard-coded. Wired through _generate into _split_text_into_chunks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYwW5KLZYovWstkx9aCwiq
- Add a "lang" field to preset voices (defaults to "fr"); documented in conf/preset_voices.json. - Show a language selector above the voice dropdown ONLY when more than one language is present, so a single-language setup stays uncluttered. Changing the language filters the voice list and resets to "Personnalisé". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYwW5KLZYovWstkx9aCwiq
- scripts/narrate_book.py: narrate a long .txt to per-chapter WAV files. Splits chapters on '---' (or --chapter-regex), chunks each chapter into sentences (reusing app._split_text_into_chunks), same seed for a consistent voice, saves one file per chapter. Memory-safe (one chapter in RAM at a time), resumable (skips existing chapter files), with a --dry-run planning mode. - docs/NARRATION.md: guide for books / guided meditation / podcast scripts — CPU vs GPU speed table, how to enable CUDA, the engine's ~8192-token limit and automatic chunking, recommended voice/settings per use case, and voice consistency via a fixed seed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYwW5KLZYovWstkx9aCwiq
… faster) CPU has no native bfloat16 acceleration, so the checkpoint's bfloat16 is emulated and slow. Mirror the existing MPS logic: upcast low-precision dtypes to float32 on CPU. Measured ~1.48x speedup (RTF 60.9 -> 41.1 on the same phrase/seed) at the cost of ~2x RAM. Opt back into bfloat16 with VOXCPM_CPU_DTYPE=bfloat16. Also document the result in docs/NARRATION.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYwW5KLZYovWstkx9aCwiq
Add an opt-in --continuity flag: each chunk after the first is generated as a continuation of the previous chunk (its audio + text as the prompt cache) for smoother prosodic joins, instead of same-seed-only. Uses a bounded 1-chunk window (never overflows the KV cache) and resets at each chapter boundary; temp prompt files are cleaned up. Mechanism validated as functional on CPU (the continuation path runs and makes normal progress, no badcase retries); it is markedly slower than plain Voice Design, so quality tuning is best done on a GPU. Default behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYwW5KLZYovWstkx9aCwiq
Extracts the long-form narration logic out of app.py and narrate_book.py
into a `narration` package that depends only on the stdlib, numpy and
soundfile — never torch or gradio. Model load alone takes minutes on a
CPU-only machine, so keeping these stages importable without it is what
makes them unit-testable at all.
Stages, in pipeline order:
text_fr French text preparation — numbers, abbreviations, roman
numerals and a user lexicon (conf/pronunciation_fr.json)
rewritten into what the engine should actually say
chunking segmentation into engine-sized pieces plus a pause plan
derived from punctuation
cache content-addressed store, so an interrupted run resumes at
the segment rather than restarting the chapter
audio trimming, de-clicking and loudness mastering
assemble chapters joined into a single MP3/M4B with markers
Covered by 192 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
Adds a book-oriented tab alongside the existing Studio one: .txt chapter import (chapters split on a lone `---`), a dry-run preview showing the segmentation, the estimated duration and the prepared text before any audio is generated, per-chapter generation resumable through the segment cache, and assembly into a single file. The chunking that used to live here now comes from narration.chunking, which also supplies the pause plan; trimming and mastering come from narration.audio. Studio behaviour is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
narrate_book.py now runs on the narration package: prepared text, pause plan, segment-level cache and mastering, instead of its own inlined copies. --assemble hands the finished chapters straight to the assembly stage. assemble_audiobook.py is the standalone entry point for chapters already on disk — join a directory of per-chapter WAVs into an MP3 or a chaptered M4B, with --check reporting loudness against the audiobook platform targets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
NARRATION.md gains the three entry points (tab, narrate_book, assemble_audiobook), what text preparation fixes and where it stops, the custom pronunciation lexicon, loudness targets, resuming after an interruption, and per-use-case settings. GUIDE_FR.md gets a short "Livres audio" section pointing at it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
A neural TTS engine fails occasionally and locally: one segment in a few dozen comes back cut off mid-word, silent, or babbling past the end of its text. On a GPU that hardly matters — regenerate the chapter. On a CPU-only machine a chapter is hours, so the only affordable repair is at the level of the bad segment, which first requires finding it. Listening to four hours of narration for eleven seconds of defect is not a method. Every check reads the waveform against the text that produced it. That pairing is what makes them possible: audio alone cannot say whether a two-second segment is complete, but two seconds for two hundred characters is a truncation. Detected are silent, truncated, runaway and clipped takes (fatal), plus internal gaps, missing decay at the end and a repeating level envelope (suspect). render_checked() re-rolls a fatally defective take with a seed derived from the original, so a repaired book stays reproducible, and keeps the best attempt rather than the last — a second roll can be worse than the first, and silently keeping the worse one would make the pass harmful. The speech-rate bounds come from measurement, not from the nominal figure: the seven preset voices span 15.8 to 24.1 characters per second on the same sentence, so the limits sit well outside that range. Checked against the real preset previews — no false positives — and a test pins those measured rates so the thresholds cannot drift back into them. audio.frame_rms_db() is exposed for this: the shape of the level curve over time is what separates a dropped sentence from a clean read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
Generation now goes through the quality pass: --qc-retries controls how many times a fatally defective segment is re-rolled (default 1, 0 to report without regenerating), --no-qc skips it, and --qc-strict exits non-zero if a defect survives, so an automated chain can react. Reused cache entries are inspected too — a segment cached by a run predating this pass would otherwise reach the book unexamined. The per-segment verdicts are written to qc_report.json next to the chapters, with the worst offenders also printed at the end. Tested end to end against a stub engine, so the whole run — planning, cache, inspection, re-roll, mastering, report — executes in milliseconds without importing torch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
A "quality re-rolls per segment" slider (0-3, default 1) in the narration settings, and defects reported in the progress panel as they are found rather than only in the summary: on a run that lasts hours, a defect worth stopping for should not wait until the end to become visible. The run ends with a count and a qc_report.json alongside the chapters. When every chapter was already generated nothing is inspected, and the quality line is then omitted rather than claiming a clean bill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
Covers what each defect code means, why only fatal ones trigger a re-roll, and the measured speech-rate range the truncation bounds are placed around. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
Clicking "Écouter un aperçu" could start a from-scratch generation instead of playing the stored sample. The handler read the description and seed from the Studio text boxes; when those were empty or out of step with the dropdown there was no seed, so no cache key, so nothing to play — and the button silently began synthesizing. The server log from a real attempt shows it: `[Voice Design] control: None`, then twenty-two minutes to reach 11% of one five-second sample, with nothing on screen to explain the wait. A preset is now asked for its own description, seed, CFG and steps, which makes the stored preview a guaranteed hit. Without one, an empty description is refused outright rather than generating a random voice, and a genuine generation on CPU warns that it will take tens of minutes and points at scripts/pregenerate_previews.py. Verified against the running server through its own API: the case that used to hang — preset selected, boxes empty — now returns the cached file in 0.9s, and the empty case errors instantly instead of occupying the queue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
Fills the gaps in the existing set rather than adding variations of it: a young female storyteller opposite the young male one, a dark female voice for thriller and noir, an elderly fireside storyteller for tales, a female counterpart to the guided-meditation voice, a plain didactic voice for essays and how-to books, a polished female voice for literary fiction, and a theatrical one for epic and historical narrative. Fourteen voices in total. Each is a (description, seed) pair, so the audio is reproducible; the previews still have to be listened to before any of them is trusted for a book. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
…ices The thresholds were derived from the seven voices that existed at the time. With fourteen previews now generated, the measured range is unchanged — 15.8 to 24.1 characters per second at both sample sizes — so the bounds stay where they are, but they now rest on twice the evidence. That a doubled sample moved neither end is the part worth recording. The expected rate moves from 17.0 to the measured median of 20.2, which only affects the "expected duration" wording in a report and the tie-break between two equally defective attempts. The test now pins every distinct rate observed across the fourteen, not a sample of four. Measured on the real previews: one voice out of fourteen is flagged, at suspect level (abrupt_end), none fatally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
The audiobook tab had no voice control at all: the book was narrated with whatever the Studio tab happened to be set to, and a reader looking for a voice for their book had to leave the tab, guess which Studio state mattered, and come back. That is how a user ends up starting a forty-minute generation while trying to listen to a preset. The tab now has its own voice dropdown and preview button, and it is the dropdown — not a copy of the Studio state — that the book is narrated with. Deliberately not synchronised with the Studio picker: two-way mirroring between tabs invites an update loop, and one authoritative control per tab is easier to reason about than two that chase each other. A custom voice still works, by leaving the dropdown on "Personnalisé" and describing the voice in Studio. The preset-versus-text-boxes resolution, written twice since the preview fix, is now a single _resolve_voice helper shared by preview and narration. The dropdown lists every voice rather than the Studio language filter's subset, so a book is never silently restricted by a control on another tab. Verified against the running server: previewing from the book tab with the Studio boxes empty returns the cached sample in under a second, and asking to narrate with no voice chosen is refused instantly instead of synthesizing a random one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
Step one no longer sends the reader to the Studio tab; the voice is chosen and auditioned where the book is narrated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
The quality pass says which segment came out wrong; acting on that meant regenerating the whole chapter — hours of CPU to replace three seconds — or hand-deleting a cache entry named after a hash. What was missing was never the audio, which the cache still holds, but the recipe: how the chapter was cut into segments and with which voice, thrown away the moment a run ended. A narration now writes plan.json beside its chapters. With it a repair is offline except for the one segment being re-rolled: read the plan, generate that segment again with a fresh derived seed, drop it into the cache under the same key, restitch the chapter from cache entries. The other segments are never touched. Three decisions worth stating. A re-roll that comes back worse than the take it replaces is discarded, because a repair that can degrade a book is not a repair. The attempt number is remembered in the cache sidecar, so asking twice gives two different takes rather than the same derived seed again. And a chapter missing a cached segment is reported rather than written, since a silently shortened chapter is worse than a rebuild that failed. inspect_book() reads the cache rather than the finished chapters — a defect has to be located at the segment to be repaired at the segment, and this way it also works on books narrated before the quality pass existed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
A "Repair a flagged segment" panel: scan the finished book, pick a flagged segment from the list, re-generate that one, and get its chapter restitched from the cache. Until now the quality report could say which segment was wrong but fixing it meant deleting a hash-named cache file by hand. Narration writes plan.json before generating anything, so an interrupted run is repairable too, and the loop consumes the very segments recorded in the plan rather than re-deriving them — if the two ever disagreed, a repair would address the wrong cache entries. The panel says out loud when a re-roll came back worse and the previous take was kept, since a repair that appears to do nothing is worse than one that reports a miss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
Previews were raw engine output, and raw output lands anywhere between -32 and -17 dBFS depending on the voice. Two consequences, both bad. The quietest presets are barely audible on laptop speakers — "Narratrice douce & naturelle" sat at -31.9 dBFS, 14.6 dB below the loudest, which is what a user hit when a preview appeared to play but produced no sound. And auditioning at unequal levels is not auditioning: loudness is heard as quality, so the comparison the previews exist for was rigged. They now get the same treatment as a chapter — trimmed, de-clicked and levelled to the narration target — with shorter edge silences so playback starts at once. A preview is therefore heard at the volume the finished book will have. pregenerate_previews.py gains --remaster, which re-levels the previews already on disk without loading the model: levelling is a post-process, so fourteen voices take seconds instead of the five minutes each that regenerating would cost. Applied to all fourteen — the spread across the set falls from 14.6 dB to 0.8 dB, the remainder being voices whose gain is held back by the peak ceiling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqWxj2j9bdavcLn25ckX8X
A .txt with `---` between chapters was the only way in. An EPUB already carries what that file has to be written by hand — reading order, chapter titles, the book's title and author — so it is read directly, in the UI and from narrate_book.py alike. Reading order comes from the spine rather than the file names, titles from the book's own table of contents (EPUB 3 nav or EPUB 2 NCX), and DRM is refused with a reason instead of narrated as noise. The part that is not obvious: a file is not a chapter. Books converted from one HTML source are cut into fixed-size files that start and end mid-chapter, so `Autour de la Lune` arrives as six 60,000-character blocks rather than its twenty-five chapters. Files holding several chapters are cut at their headings, and a file's opening fragment rejoins the chapter it continues. Which heading level marks a chapter cannot be decided inside one file — a lone <h1> above repeated <h3> is a packed file, and equally a chapter above its scenes — so it is decided by which level opens the documents across the whole book. Verified against two real books of differing structure: Autour de la Lune (25 chapters, 1859 segments) and Les trois mousquetaires (72 chapters). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fity58qgKttpD1nLheWrzy
Two passages of every imported book were being read aloud that no listener wants. Project Gutenberg wraps each work in an English notice and closes it with the full licence — some 17,000 characters, around twenty minutes of legalese, in the wrong language, at the end of a French audiobook, and hours of CPU to synthesize. And a book whose own table of contents sits in the text opens on several minutes of chapter titles read one after another. The Gutenberg cut is exact rather than heuristic: the `*** START OF … ***` and `*** END OF … ***` lines are part of the format, so they are the only thing cut on, and a book carrying neither is returned untouched. Removing the header orphans the opening chapter's title, which came from a heading inside it, so the title is re-derived from the title page left behind. A contents page gives itself away by having nearly every line equal to the title of another chapter, which prose never manages; the threshold is blunt on purpose so a real chapter cannot trip it. Nothing is removed silently: every cut is reported in EpubBook.removed, shown in the plan and under the upload button, and `--keep-boilerplate` turns the whole pass off. On the two books tested end to end: 19,066 characters removed from Autour de la Lune, 21,742 from Les trois mousquetaires (contents page included). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fity58qgKttpD1nLheWrzy
A finished audiobook is not just the book read aloud. ACX and Audible, and behind them Amazon, Apple Books, Kobo and Google Play, all require the recording to announce itself: the first file opens on title, author and narrator, the last one names them again. A submission without that is rejected at quality review before anyone hears a line of the prose. This fork produced neither. The credits are added as chapters, not as a special case. They then take the same French preparation, the same voice and seed, the same mastering and the same cache as the book — so they sound like the narrator rather than an announcement bolted on afterwards, and they resume and repair like anything else. Where no human narrator is named, the credits say the reading is a synthetic voice. That is the default and turning it off takes a deliberate flag: Audible distributes such titles through a separate programme and labels them, and passing a machine reading off as a performance is what closes an account. The same standard governs the shape of a delivered file, not only its level — 0.5 to 1 second of room tone before the first word, 1 to 5 after the last, no file past two hours. The mastering defaults sat at 0.3 and 0.6 seconds, below the floor: a chapter with textbook loudness was rejectable on shape alone. They now aim at the middle of each window, and acx_report measures all six limits instead of three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fity58qgKttpD1nLheWrzy
An M4B is what you listen to. It is not what you upload. ACX takes one file per chapter at a fixed specification, plus a retail sample, and rejects the lot over details that have nothing to do with how the narration sounds. This fork produced a 96 kbps MP3 or a 64 kbps M4B of the whole book: unusable for a submission, however good the reading. scripts/export_acx.py turns finished chapters into that folder, and narration/delivery.py holds the parts worth testing on their own: - 192 kbps CBR MP3 at 44.1 kHz, mono, resampled at encode time. Constant bitrate is why -b:a carries no quality flag; a VBR file is refused whatever it sounds like. - The duration limit is computed, not assumed. 120 minutes and 170 MB are close enough at 192 kbps to swap places, so whichever binds first decides (~118 min). The size cap is read in its strictest sense, because being under a limit that turns out to be looser costs one extra file and being over one costs a rejected submission. - A chapter past that limit is cut in a pause, never mid-word — at the *last* pause before the limit, not the quietest moment in the window, which can be a dip inside a sentence twenty seconds earlier. The search never reaches back past 60% of the limit, which is what stops a chapter with no pause at all from being split into thousands of empty files. - A retail sample of 1 to 5 minutes, taken from the first real chapter and never from the credits: nobody decides on hearing the title read out. - Every file checked against the whole specification, with the reasons in plain French, and a non-zero exit so this fits a pipeline. Without ffmpeg the WAVs and the exact commands are still written. Hours of synthesis must not be held hostage to a missing binary. Verified end to end on real generated audio, which the check correctly refused: never mastered as chapters, it has no room tone and sits outside the loudness window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fity58qgKttpD1nLheWrzy
The delivery rules were only reachable from a script, which is not where the book gets narrated. The audiobook tab now has a compliance button that reads the finished chapters and says, chapter by chapter, what a distributor would accept or send back — loudness, room tone at both ends, duration, predicted file size — with the reasons in plain French. It reads and encodes nothing. ffmpeg may not be installed, and the answer worth having before an evening spent uploading is whether the files pass, not the MP3s themselves; the full export stays one command away and is printed under the table. narrate_book.py also takes --export-acx, so a book goes from text to the folder that gets uploaded in one command. It runs the exporter as a subprocess rather than importing it: a book that narrated for nine hours must not lose its chapters to an exception raised while preparing the delivery. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fity58qgKttpD1nLheWrzy
The encoder settings were hard-coded, so an M4B was 64 kbps AAC and nothing else. That default is right and stays: 64k AAC mono is around what Audible itself streams for a finished audiobook, and speech gains very little above it. What was wrong is that it could not be changed — an archive copy, or a file that will be re-encoded downstream, is worth more. --bitrate on both scripts and a Débit menu in the audiobook tab. The MP3 default moves from 96k to 128k, which is what MP3 needs to match the AAC at 64. A bitrate is accepted however the user writes it (128, "128", "128k") and nonsense is refused here rather than handed to ffmpeg to fail on later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fity58qgKttpD1nLheWrzy
scan_risky_words.py was written to find the words an engine says badly, by looking at what the text contains rather than by listening to ninety hours of it. The first run found something else: 993 bracketed markers across the twenty books queued for narration, 969 of them "[PAUSE]", in ten books. None of those ten had been narrated yet. They are not one thing, and treating them alike would have been wrong twice over. "[PAUSE]" is an instruction to stop talking, so it becomes a paragraph break and the pause profile turns it into real silence. "[rire]", "[silence prolongé]", "[pleurs contenus]" are stage directions in transcribed testimony — they tell a reader what happened in the room, and spoken aloud they announce that the narrator laughed, so they go. But "[nom du département]", "[ton mari / ta femme]", "[date]" are the sentence itself, a blank the reader fills, and deleting them leaves a hole where the meaning was: they keep their words and lose only their brackets. Brackets are never spoken. What is inside them sometimes is. Verified after regenerating all twenty prepared texts: zero brackets remain. scan_risky_words.py itself is kept — it ranks acronyms, proper nouns and foreign words by how often the corpus says them, because a sigle read eighty times costs eighty mistakes and a name read twice costs two. It corrects nothing; it feeds try_pronunciation.py, and only what has been heard goes into the lexicon. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
…rson A catalogue read end to end by the same voice deserves to credit it by name, the way a publisher credits a virtual voice. The obvious way to do that — passing the name as --narrator — would have been wrong: that field means a human narrator, and filling it is exactly what suppresses the synthetic-voice disclosure that Audible, Apple Books and Findaway require. The credits would have announced a performance that never happened. So narrator_credit grows a third case instead of reusing the first. A human narrator is named and stands alone. A synthetic voice with a name is named *and* disclosed — "Lu par Aurore Cabonet, une voix de synthèse." A synthetic voice without a name is disclosed as before. Turning the disclosure off remains a deliberate act, unchanged. narrate_queue.py credits Aurore as "Aurore Cabonet" and Alex Somerset as "Gabriel Adam" across the queue, and a book may override it per entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
"HFD" appears 160 times in one book and nowhere else in the catalogue. It is never expanded: the text uses it from chapter one onward. A reader turns back a few pages or infers it; a listener hears "ache-èf-dé" a hundred and sixty times and never learns what it stands for. Edwin chose to expand it everywhere. The lexicon could already do that, but --lexicon took a single path and *replaced* the default, so a book-specific file would have silently dropped SNCF, RATP, Wi-Fi and Nietzsche. It is now repeatable and the files stack, the later winning — a book adds to the general lexicon instead of standing in for it. narrate_queue.py reads an optional "lexicons" list per queue entry. The expansion needs three rules, not one, and the lexicon's longest-key-first order is what makes them work together. "les HFD" becomes plural, because the three occurrences already carry feminine plural agreement after them — "non encore prises en charge", "non aiguës". "mécanisme HFD" becomes "mécanisme de la dépression…", since the apposition needs the article. Everything else is the plain expansion. Verified on all six real contexts drawn from the book. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The opening credits of "LE CERVEAU VOLÉ" said, aloud:
livre-un-esprits-reprogrammes.
A .txt carries no metadata, so narrate_book falls back to the file name, and
the French normaliser helpfully read "livre-01" as "livre-un". Five delivered
books opened and closed this way — every one produced from a prepared
manuscript. Only the book imported from EPUB was right, because an EPUB names
itself.
The queue now carries title and author per entry, taken from the manuscript's
own headings, and passes them through. Verbatim: an earlier attempt to recase
LE CERVEAU VOLÉ into title case produced "Les Marchands D'immortalité" and
"Dormir, c'Est Apprendre", and each rule fixed invented another. The casing
changes nothing in the ear — the normaliser leaves capitals alone rather than
spelling them — so the author's own wording stands.
The five books already delivered still carry the wrong credits. Their masters
were swept, so a repair means regenerating the two credit chapters and
replacing those two files in the ACX export, which is a separate job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The delivery reports told a story the ear had not:
Le Cerveau Volé Alex Somerset floor -58.2 dB 18/23 rejected
Atlas des Villes... Alex Somerset floor -57.9 dB 15/21 rejected
Les Effacées Aurore floor -68.3 dB all pass
Le Lundi de Trop Aurore floor -71.1 dB all pass
ACX refuses anything above -60 dBFS. A cloned voice inherits its reference's
room tone, and the Alex Somerset reference was the noisy one — its own note
says so: a constant floor at -52 dBFS where Aurore sits at -71. Nine books in
the queue carry that voice; every one of them would have come back.
The noise sits in 80-150 Hz, twelve decibels above the next band and just above
the 80 Hz high-pass that was supposed to catch it. Raising the high-pass would
have worked and would have thinned the voice: that band is also a male
fundamental. So the fix acts in time rather than in frequency — a downward
expander, threshold relative to the chapter's own speech level like the
compressor, reduction capped at 16 dB because a silence dug out too far is
heard as a hole.
Measured on real chapters: Alex Somerset -58.7 to -72.3 dB, Aurore -72.9 to
-88.8. Speech loses about two decibels, which the normalisation immediately
after puts back.
The two Alex Somerset books already delivered keep their floor; their masters
were swept, so they need re-narrating rather than re-mastering.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The previous commit added a downward expander to the chain and left test_nothing_happens_when_everything_is_off asserting that a settings object with highpass, deess, compress and limit disabled is inert. It no longer was, because expand defaults to on — correctly, since the whole point is that books get it without being asked. The test is right and the omission was mine: a new stage belongs in the list a caller turns off. Pushed one commit late, because the push ran whether or not pytest had passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Three hundred books is the constraint that changes the method. Correcting pronunciation needs to know which words sound wrong; knowing that needs an ear; and nobody will listen to three hundred books. So the referee cannot be an ear. It is speech recognition. Have one machine read back what another just said, and compare with the text it was given. Where the transcript diverges, the pronunciation is suspect: Whisper does not invent "Guébrou" if it heard "Gebru". Nothing needs re-narrating for this. The segment cache already holds the text asked for beside the audio produced — exactly the two terms of the comparison. Two refinements the first tests demanded. Comparison is by multiset rather than by alignment, because one swallowed word shifts everything after it and the question is which words are wrong, not where. And a correctly spelled acronym comes back as separate letters — "T. D. A. H." — which is the right pronunciation written differently, so runs of single letters are glued back before comparing; without that the report drowns in acronyms that were fine. What it cannot see is stated in the module: Whisper corrects what it hears against meaning, so "ce livres" is transcribed "ces livres" and grammatical words escape the audit. Those are for the ear. Proper nouns, acronyms, foreign words and numbers have no grammatical safety net, and there the divergence is plain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The model loads in half precision, the features arrive as float32, and the first convolution refuses the pair: "Input type (float) and bias type (c10::Half) should be the same". Cast to the model's own dtype instead of naming one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The first real run buried its findings. Sixteen words came back above the threshold and thirteen were noise of two kinds. Numbers: the French normaliser writes "mille neuf cent quatre-vingts" and Whisper writes "1980". The pronunciation is right and only the spelling differs, so numerals are excluded — testing each part across hyphens, because "quatre-vingt-dix" is as much a number as "dix". Grammatical words: the module already documents that Whisper corrects what it hears against meaning, so "ce livres" comes back "ces livres". Those words can only appear by transcription accident, and in numbers they make the report unreadable. They belong to the ear, and the ear is what caught "ces". Truncated segments are excluded too. Their words are missing because they were never spoken, not because they were spoken badly, and the quality pass already handles them — counting them here surfaces perfectly good words as suspects. What remains is what the method sees clearly: proper nouns, acronyms, foreign words. Verified on the cases that produced the noise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Nothing existed for this. The audiobook-producer skill sitting in Downloads covers the editorial side and its TTS step says only "choose a service"; every installed skill is about web deployment. So the method this session paid for was living in commit messages. It is written as a skill and installed at ~/.claude/skills/audiobook-text-prep, with this copy versioned beside the code it describes. The framing matters more than the checklist: a preparation fault is not visible when it is made. It surfaces three GPU-hours later, in a finished book, and it repeats identically in every book after. On a catalogue it does not cost a book, it costs the catalogue. Every trap listed was paid for once — the `---` collision, the table of contents read aloud, 969 [PAUSE] markers, sentences truncated past 300 characters, two-character fragments, an abbreviation used 160 times and never expanded, five books announcing their own file name. It also states plainly what the ASR audit cannot do. Whisper corrects what it hears against meaning, so grammatical words escape it — "ces" pronounced "ce" was caught by an ear and the next one will be too. A method that overclaims is worse than one with a known blind spot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Edwin wants the credits to name the narrator and stop there. The mechanism was already there — disclose_synthetic has always been settable — but nothing exposed it, deliberately, because it is not a comfort setting. It is now a flag on narrate_book and on the queue, and a per-book field so the choice is visible in the data rather than buried in a command line. Stated once and not repeated: ACX, Apple Books and Findaway require generated narration to be identified as such, and "Gabriel Adam" and "Aurore Cabonet" are voices, not people. Removing the line presents a synthesis as a performance and risks rejection or withdrawal after publication. That is the publisher's call to make, and he has made it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Every book reported "19 segment(s) réparé(s)" and then, on the next line, "réparation incomplète". Both came from the same run and only one was true: the count was the number of fatal segments *found*, and repair_segment keeps the older take whenever the new one is worse — which it says, in as many words. Measured once on a real case: 0.6 s of babbling became 1.6 s on the retry, and the old take was rightly kept. A defect that cannot be re-rolled away is exactly the kind Edwin is hearing, so the number that matters is how many actually improved. The runner now counts what repair_segment reports rather than what the attempt hoped for, and records both: fatal_found and repaired. A book whose repairs all failed now says so instead of claiming nineteen fixes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Two things were missing from the listening copy, and Edwin found both by putting a book on his phone. No cover. Forty-six books in the catalogue carry one, named audio-cover.jpg — made for exactly this — and none of them reached the M4B. The queue now carries a cover path per book and narrate_book already knew what to do with it. And the chapter list read "Chapitre 1", "Chapitre 2". The marker title is the first line of the prepared text, and a manuscript writes "# Chapitre 1" with the real formulation underneath as "## Le mythe de la croissance infinie". The two are now joined, so the phone shows a table of contents worth reading and the narrator announces the whole title rather than a number. The rule stays cautious: it fires only on a heading that says nothing but its rank, and only when what follows is short and unpunctuated — a subtitle, not a paragraph. One chapter in eighteen kept its bare title, which is the correct outcome for that chapter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Edwin proposed a preprocessing layer with per-domain pronunciation dictionaries. Most of what he sketched already exists — the STT feedback loop is audit_pronunciation.py, lexicons already stack, segmentation already works on sense groups, punctuation already drives the pauses — but the domain dictionaries did not, and his catalogue is specialised enough to need them. The engine settles the rest of the design. It has no phoneme, IPA, SSML or g2p path anywhere: an SSML tag tokenises to twenty tokens and would be read aloud, and the IPA symbols exist in the vocabulary only as ordinary characters, since the model was trained on graphemes. The cleanest option in his plan is therefore unavailable, which makes respelling — the fallback he rightly warns against — the only lever there is. One part of the design is not needed. "ces" and "ces" are not homographs: "ces" is always /se/ and "ce" always /sə/, and the engine is not choosing wrongly between them, it is swallowing the final s. No part-of-speech tagger changes that. Context does matter for the real homographs — plus, est, fils, couvent — and the lexicon has carried that since before this session. Entries ship disabled, with the measured frequency of each term across the twenty books beside them: TSPT 89 times, cortisol 60, EMDR 59, accumbens 3. A term read eighty-nine times costs eighty-nine mistakes. Nothing is enabled until it has been heard to be wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Edwin heard it in the delivered books and named it precisely: the engine says "ce" where the text says "ces". He listened to four spellings and chose "cés". "ses" gets "sés" by the same logic — restore the accent, restore the /e/ — applied without a separate listen because the parallel is exact, and marked as such in the file so it can be pulled if it turns out otherwise. No context rule is needed. "ces" is always /se/ and "ce" always /sə/: they are different spellings, not homographs, and the engine is not choosing wrongly between them but swallowing a letter. That distinction is why "plus" and "fils" carry context conditions in this file and these two do not. _apply_lexicon now restores capitalisation. Matching ignores case, so "Ces" at the head of a sentence was becoming "cés" and opening a sentence in lower case for no reason. The rule applies to every entry, not just the one that revealed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The first book to carry the new markers showed the flaw: "Chapitre 1 — La fin du culte du charisme — Pourquoi l'extraversion à cessé d'êtr". The subtitle already carried its own dash, joining added a second, and chapter_title cut the result at eighty characters mid-word. A subtitle that dashes into a longer gloss has its real title in front. Take that part and leave the gloss out — the marker has to read on a phone, not carry the whole sentence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Edwin reported "degré" coming out as "degre", and the lexicon was the obvious place to fix it. It was the wrong place. The manuscript writes "degres", without the accent, and the engine read exactly what it was given. Measured across the queue: twenty-two files of twenty-four carry the defect. 31 "maniere", 17 "difference", 7 "degre" and "degres", plus "plongee", "societe", "desir", "dedicace". Every one of them reaches the listener as a pronunciation fault that nobody made at synthesis. Accents are now restored in the text, which is the only honest repair — a lexicon entry would have replaced a misspelled word with an invented spelling and stacked two approximations instead of removing one. The list is short and hand-checked, and holds only words whose unaccented form is not a French word. "cote", "tache", "sur", "mure", "pecheur" all have two legitimate readings and are deliberately absent: correcting those blind would trade one fault for another. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
"0 amélioré(s), 3 inchangé(s)" — the first book where every repair failed. Two
of the three defects were these:
ch002/seg001 1.44s 6 car/s « Dedicace »
ch003/seg019 1.44s 6 car/s « EPILOGUE »
A single word takes about a second however long it is, so measuring characters
per second says nothing at that scale — and the runaway rule, which fires below
six, declared both of them broken. The repairs then failed because there was
nothing to repair, and the report claimed three defects where there was one.
The rate rule now applies only above twenty-five characters. Below that,
duration carries no information about the text, and min_duration_sec already
catches a generation that returned nothing.
The third defect was real: a whole paragraph returned in 0.64 seconds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The previous commit stopped the rate rule below twenty-five characters, so a lone chapter title would no longer be called a runaway. It also stopped catching a genuine one: three characters returned as twenty seconds of audio is broken, and the existing test said so. Below the length threshold, duration alone judges. A word takes about a second however long it is, and never ten. "Dédicace" at 1.44s is healthy, "Dédicace" at twenty seconds is not, and both are now decided correctly. Pushed the previous commit while that test was red — the push runs after the commit whether or not pytest passed. This one was gated on a green suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
AssertionError: assert len(input) > 0
wetext/token_parser.py, load()
The narration of "JE ME SOUVIENS" died there, and the segment that did it read:
le DSM-5-TR (Diagnostic and Statistical Manual, 5ᵉ édition révisée…)
"5ᵉ" carries U+1D49, MODIFIER LETTER SMALL E. It looks like an ordinary "e" and
is not one, so the ordinal rule never sees it: "la 5e édition" normalises to
"la cinquième édition" and "la 5ᵉ édition" passes through untouched, all the way
to the engine's own normaliser, which chokes on a token that reduces to nothing.
Eighteen occurrences across nine files of the queue. Every one of them was a
book waiting to fail at whatever minute it reached them.
Superscript modifier letters are folded to their plain forms first thing in
_clean_typography, before any rule that reads them — which fixes the crash and
the pronunciation in the same move, since "5ᵉ" now becomes "cinquième" rather
than whatever the engine would have made of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
A systematic sweep of the twenty-one queued books turned up forty-three characters outside ordinary French, and most of them reach the listener. The counts are the argument: 571 fill-in rules, 163 footnote markers, 107 middle dots, 55 word-processor commands, 47 degree signs, 26 ampersands, 21 interface paths, 20 arrows, 8 checkboxes. One was worse than untouched. "\newpage" lost its backslash to the markdown cleaner and became "ewpage", pronounced as a word in the middle of a chapter. Word-processor commands are now removed first, before anything can strip the backslash that identifies them. The rest become words: 18,5 °C is spoken in degrees Celsius, "&" is "et", "×" and "=" are read, arrows and "Réglages > Temps d'écran" become "puis", and "conjoint·e" is read out as "conjoint ou conjointe" — which needs the whole word to rebuild, since the ending alone gives "conjoint ou e". Form leftovers, checkboxes and footnote asterisks are dropped, because they are page furniture and not speech. Order matters in one place and the test says so: without the Celsius rule before the general degree rule, "18,5 °C" becomes "18,5 degrésC". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The pronunciation audit worked and was useless at scale: three hundred books would produce three hundred reports, and nobody reads three hundred reports. What is wanted is a single ranked list, fed by every book, reviewed once. --merge accumulates into one file, counting occurrences across books and recording which books each word came from. A word heard wrong in eight books gets corrected before one heard wrong once, at equal listening time — the same ordering principle the risky-word scan already uses. The runner can now run it per book, and does so before the sweep rather than after: the audit reads the segment cache, and --keep deliverables deletes it. Getting that order wrong would have made the whole thing silently empty. It stays opt-in. Whisper costs GPU minutes per book, and a catalogue owner should decide whether to spend them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The runner called audit_pronunciation with --merge, the merge code was there, and argparse rejected the flag: "unrecognized arguments: --merge". The audit died in under a second, the runner logged that it had run, and the ranked list was never written. The cause is a script of mine that did two replacements and asserted between them. The second assertion failed, so the file was never written at all — and the first change, the argparse option, went with it. I then added the merge logic with an editor and never noticed the option was missing, because the runner reports that the step ran, not that it worked. Two lessons, one of them already applied: a step that cannot fail loudly should not be trusted quietly. The audit now runs and writes; the next book will produce the first ranked list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Three books died on the same line, at 41, 69 and 98 minutes in, each after
everything expensive had already succeeded:
assert len(input) > 0 wetext/token_parser.py
The first was traced to "5ᵉ", a superscript letter, and fixed there. The third
had no superscript at all. The trigger was never the point: clean_text strips
markdown, emoji and newlines, and when a fragment is nothing but those, it
hands wetext an empty string, which refuses it with an assertion that unwinds
the entire run.
Chasing triggers one at a time was a losing race. The call is now guarded: an
empty result returns instead of raising. Nothing downstream needed the call to
have happened, and an empty segment is already caught by the quality pass and
by the badcase ratio guard added earlier today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The previous guard was in the right file and did nothing. It checked for an empty string before the call; the text reaching it was not empty. wetext's own tagger reduces some inputs to nothing and then asserts on its own intermediate result — the failure is inside the library, past anything a caller can inspect. So the call is wrapped instead of predicted. Any exception, or an empty result, returns the text untouched. Normalisation turns "5" into "five" and tidies spacing; the raw text is a serviceable fallback, and a chapter with one un-normalised sentence beats a book that does not exist. Catching broadly is deliberate here. The failure to guard against is *any* failure in third-party text handling, not a specific exception — and this is the fourth time this session that a book of three hours died on a line that had nothing to do with speech. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
The first real ranking came back with 180 entries and most were noise: "l'enfant", "parent", "anxiété", "volume" — ordinary French that nothing was wrong with. Reading the contexts explained it. Those words all came from segments whose transcription diverged wholesale: bibliographies, lists of resources, a take that went astray. One bad segment contributes every word it contains, and eleven such segments bury the one finding that mattered — "Filliozat" heard as "Filiozat", a proper name with a letter too few. A segment now has to have at least sixty percent of its words found before any of them counts as suspect. Below that the take is botched, not badly spoken, and the quality pass already reports it. Measured on the two shapes: a sentence with one mangled name matches 78% and is kept, a wholesale divergence matches 20% and is dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Three books audited, 264 words flagged, none of them seen in more than one book. A signal that never repeats across a catalogue is not a signal. The cause is what speech recognition is: Whisper does not transcribe word for word, it paraphrases — drops a hesitation, rewrites a turn of phrase. An ordinary word missing from the transcript proves nothing, and at any tolerance loose enough to keep real findings, hundreds of them get through. A proper noun and an acronym have no such cover. Whisper does not write "Filiozat" for "Filliozat" if it heard the name correctly, and it was that one finding, buried under 179 others, that showed what the tool is actually for. So the filter now keeps capitalised words and runs of capitals, and drops the rest. The known cost: a lowercase foreign word — "mindfulness" — is no longer distinguishable by shape from ordinary French and falls out. That gap is covered by scan_risky_words.py, which finds foreign words by spelling pattern rather than by transcription. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Une parenthèse ne s'entend pas, mais le modèle la traite comme une paire à refermer et abandonne au milieu d'une longue énumération. Sur les vingt premiers livres, 86 % des segments tronqués contenaient une parenthèse, contre 29 % des segments en général — et 30 % des segments de même longueur, donc c'est la construction qui coûte et non la longueur. « Les approches alternatives (keynésienne, institutionnaliste, marxiste, écologique) » sortait en 2,1 s d'audio pour 260 caractères. Les incises courtes — une date, une source — sont laissées telles quelles : elles n'ont jamais tronqué. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
Une reprise écrit par-dessus la production précédente, mais seulement là où les noms de fichiers coïncident. « Le Pouvoir Silencieux » est ressorti avec vingt-huit fichiers ACX pour vingt chapitres : sept rescapés de la prise du 8 août, portant des titres à peine différents. Le M4B était sain — c'est le jeu qu'on livre à une plateforme qui ne l'était pas. Le dossier `.cache` est épargné : c'est lui qui permet à une narration interrompue de reprendre où elle en était. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBNmTCfFPDth6cWqg69MGu
L'audit de prononciation du catalogue a signalé 166 mots. Presque aucun n'était mal prononcé : ils étaient les débris de trois constructions que la narration lit sans les comprendre. Les lignes à remplir. Le trait était bien effacé, mais lui seul : « Jour 5: ___ minutes (objectif: 10 min) / Ressenti: ___ » devenait « Jour 5 : minutes (objectif : 10 min) / Ressenti : », narré tel quel et relu par l'audit en « Jour 5, minute objectif, 10 mines, essenci », sept fois de suite, dans un livre déjà livré. Ce qui suit le trait n'a de sens qu'avec lui — « ___ heures ___ minutes » énonce des unités sans grandeur — donc le trait emporte sa queue, jusqu'à la parenthèse suivante, qui porte souvent la seule information de la ligne. L'intitulé, lui, reste toujours : c'est une consigne que l'auditeur peut suivre. Trois livres de la file portaient ces lignes, 68, 11 et 8, et deux sont encore devant nous. Les intervalles chiffrés, 726 dans les vingt et un livres. Le trait d'union se dit « à » ; ne pas le dire ne laisse pas un blanc, il colle les deux nombres et la passe des nombres les fond en un seul. « La pandémie de 2020-2022 » se narrait « deux mille vingt-deux mille vingt-deux ». Les chaînes plus longues sont épargnées : « 4-7-8 » est une respiration, pas un intervalle, et « 5-4-3-2-1 » un exercice d'ancrage. Les minutes abrégées, 106 fois. « 20 h » était déjà lu par la règle des heures ; « 5 min » ne l'était par personne et se disait « min ». Une limite reste, notée dans le code : la passe des nombres ignore le genre du mot qui suit, donc « 51 min » — une occurrence dans tout le catalogue — se dit « cinquante et un minutes ». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MErTnGqADwhFXyMoCb4oC4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Claude-Session: https://claude.ai/code/session_01MeWfqpDWJs9veHKHRzQqG9