Changelog
All notable changes are documented here. Libris follows Semantic Versioning; while the project is below 1.0, minor versions may include breaking operational changes that are called out explicitly.
[Unreleased]
[0.28.0] - 2026-10-05
Upgrade notes
- One database migration (
8e2f4a6c1d93): the translation memory key of the passages holding verse is recomputed. Nothing else changes; the other keys are identical.
Changed
- The book’s Quality tab has a Show resolved button: it loads the alerts already handled (
history=true), each with its “Resolved” badge, and Hide resolved comes back to the open ones. Resolved alerts had not been shown since the list only returned open ones. Closes #378. - The style sheet now labels
guillemetsas “Quotation marks of the target language”; the French user guide describes the target language’s marks instead of French ones. - A locked glossary term that no answer satisfies no longer costs the book its corrections, nor its export. Corrections of a text already translated (arbitration, an accepted proposal, a revision after review, a polish, and their repair in small groups) are refused for a locked term only when they lose one the text had; a term already missing no longer refuses the correction of another point of the passage, as the merged review already did. A first translation still has to carry every locked term. At the end of the rounds, a locked term still missing no longer blocks: the error earns the passage every remaining round as before, then the passage is closed on its text and listed in
residual_pointswith the new"motive": "locked_term_unmet"and the term (source → expected translation), and the report iscompleted_with_residualsinstead ofblocked. Thelocked_termalert stays open on the quality page. Text left in the source language, a check without a verdict and an error the arbiter accepted and could not apply still block. Seedocs/autopilot.md. Closes #379. - Locked glossary terms: between the words of a locked translation, a space and a hyphen (
-, U+2010, U+2011) are now the same spelling, both ways and in every language written with spaces; each word still takes its own agreement. Two neighbouring locks such asmaid robots → robots-servantesandmaid robot → robot servantecould not both be satisfied by one sentence, and every translation of the passage was refused. A term really left out is still an error. The analysis no longer locks a name whose translation is written as it is in the text of the book (ototo → younger brotherin a book translated from English): it is a gloss in the source language, kept as the preferred rendering, with askippeddecision in the journal. Refs #379. - The semantic fidelity check is now read by the configured quality judge (the book’s, else the installation’s) instead of the model that translated. Network: with an installation judge, every book is sent to that provider for this check, a book translated on a local model included; No judge for this book keeps the check on the book’s own provider. Without a judge, or when the judge does not answer, the book’s provider reads as before. Each proof records the provider that read (
provider_id). Seedocs/quality.md. Closes #360. - A reviewer’s remark now says which words it is about: each critique may carry
source_quote(the words of the source that show the problem) andtranslation_quote(the faulty wording), asked for by the four review prompts (translation_review,quality_judge,review_revision,final_review, prompt versionfile-v32) and by the JSON schema itself, so a customised prompt gets the instruction too. Both are optional: an answer without them is read as before. When a rewrite is read again, two remarks of one category on one paragraph are now two defects when their source extracts stand at different places of the source, even if neither quotes anything in its description; extracts that overlap are one defect, however the rewritten translation is quoted. An extract the source of the paragraph does not say is ignored, and remarks without extracts (stored before the update, or left empty by the model) are matched as in #359. Positions are character offsets, so Japanese and Chinese are placed like any other language.backend/eval/critique_anchors.pymeasures, on a pre-production, how often two readings of one text are told apart. Closes #363. - The interface no longer downloads every screen before showing the sign-in page. The first JavaScript file held the settings, the workspace, the readers, the statistics and the documentation — 319 kB gzipped, for a visitor who only wanted to sign in. Each screen is now fetched when its route opens: 100 kB for the sign-in page, 156 kB for the library, 249 kB for a book with its reader. A screen shows a loading state inside the shell while its file arrives, and one whose file can no longer be fetched (an update replaced it) offers to reload the interface. The build now fails when the first file grows past 130 kB gzipped (#345).
- Internal: the book panels of
frontend/src/features/panels.tsx(2,300 lines) now live in one module each —ProjectSettings.tsx,Glossary.tsx,Bible.tsx,Quality.tsx,Observability.tsx— each with its own labels and translations. No behaviour, text or bundle chunk changes (#347).
Fixed
- Autopilot: a reviewer’s meaning error is blocking whatever name the model gave its category. The prompts ask for one of fourteen category names, but nothing checked it, and the server compared the name exactly: an
errorfiled undersens,Mistraduction,contresensorfidélité(24 of them on one book) was a point of detail — no further round, no stronger model, and the passage was closed as an exportable residual. The category is now read through a table of other names (case, accents, spaces and hyphens ignored) where every critique is read, so critiques stored by earlier versions are covered too, and two rounds that name one defect differently are compared as the same defect. Anerrorin a category Libris does not know is blocking and logged (critique category … is unknown) instead of passing as a detail; awarningstays a warning, and a terminology error, under any name, still stops the book only for a locked term. Closes #382. - The analysis no longer takes a pronoun or a common noun for a name. On a French → English first-person novel it had accepted then locked
moi → Narrator,mon père → Dad,ma mère → MotherandElle → Florenceas names, and keptla conseillère en orientationin French under a policy that keeps names: 245 answers were refused formoi → Narratoralone and a third of the passages ended in error. A name proposal whose source or translation is a pronoun or a designation of a character (narrator, parent) is now removed, with arejecteddecision in the journal. A name written without a capital in a language that has capitals (la psychologue,juge) is never locked nor frozen in the source language: it stays the preferred rendering, with askippeddecision. A proper name (Florence) is locked as before, and a term a person decided is never touched. A book already analysed keeps its locks: remove them in the Glossary tab, they are not proposed again. Closes #381. - A locked name whose translation keeps the original form with a hyphen (« Ino » → « Ino-sama », « Sora » → « Sora-san ») no longer raises « Forme d’origine restée dans la traduction » on a text that writes it: « Ino » inside « Ino-sama » was read as the source form, so no answer could satisfy the lock. The form is now reported only where it stands outside the locked translation (« Ino » alone); a hyphen and a space are one spelling, as in #379. And an alert on a locked term is no longer closed by the arbiter’s rejection: the arbiter’s verdict is logged, the alert stays open until the text carries the term, and a passage closed without it is listed in the report (
residual_points,motive: locked_term_unmet) instead of leaving the lock neither met nor reported. Closes #380. - A style sheet that asks for quotation marks (
dialogue: guillemets) no longer sends French typography to a book translated into another language. A German book received “French quotation marks « » and an em dash for each change of speaker” and came out as— «Warten Sie hier», hat der Fährmann gesagt.; it is now asked for the target language’s own marks („ “ for German, “ ” for Brazilian Portuguese against « » for Portugal, 「 」 for traditional Chinese), and only a French target keeps « » with the dash per speaker. The dialogue check follows: on such a book it reports straight quotes only, naming the target’s marks instead of the French ones. Closes #365. - A descriptive noun phrase the analysis took for a name no longer leaves its passage in the source language and the book
blocked. Chinese → English, « 黑衣人 » → « black-clad agent » was locked as a name: every translation of the chapter was refused (« horsemen », « the black-clad leader », a possessor English leaves out) and the original text was kept. Three changes. The autopilot no longer locks a translated name written entirely in lower case: it stays in the glossary as the preferred rendering, and the decision log says so (skipped); a proper name (« Li Santong ») is locked as before, and a term a person decided is never touched. A locked name written in lower case that a paragraph does not carry is now a warning, not an error; a proper name and any other locked term are still errors. And when the recovery ladder is spent and answers were refused for their locked terms alone, the one that missed the fewest is kept — after the fidelity check — marked to check, with the alertrecovery_kept_translationnaming the terms; such a passage no longer blocks the book. An answer with any other error (text left untranslated, a copied paragraph, a suspicious length) still gives way to the original text. Closes #374. - On a phone, a book with a long title no longer makes the whole page scroll sideways: the last item of the breadcrumb is cut with « … » as intended. The breadcrumb kept the full width of its unbreakable title (515 px in a 320 or 390 px window) because it could not shrink inside the page header. Short titles and wider windows are unchanged. Closes #375.
- The book’s Glossary tab no longer scrolls sideways on a 768 px wide screen: the native file fields of « Take terms from an existing translation » kept their intrinsic width and pushed past their grid column (page 800 px wide). A file field now never grows wider than its container, on every screen. Closes #376.
- Chinese → English: a material named in the source and dropped for a general word (古玉 rendered « fine antiques ») is now caught and corrected without anyone stepping in. A new automatic check,
named_material, reports a paragraph whose source names jade, jadeite, amber, agate, coral, ivory, pearl, bronze or a precious stone (diamond, sapphire, ruby, crystal…) when the translation carries none of the matching English words; a word inside a glossary term (a name) or a fixed compound rendered without the material (玉石俱焚, 琥珀色) is not counted. The final review now also reads the automatic checks as they stand on the current text, not only the alerts recorded when the passage was written, so a passage translated before a check existed gets its correction on the next review. The list is hand-written for this pair and covers materials only: colours and other named objects still rely on the reviewers’ rules. Closes #362. - A Book Bible the autopilot validated on its own is no longer presented to the translator, the reviewers and the judges as a person’s decision.
EDITORIAL_BOOK_CONTEXTkeepsstatus: inferred(lowest authority) whilebible_auto_validatedis set, the narrative context reports its origin asanalysis, andSERIES_CONVENTIONSnow says that a person’s unlocked series decision (origin: series_decision) does not yield to the book’s own choices: an inferred “keep Elysia” no longer overrides the series decisionElysia → Élysia, and the quality judge and the final review no longer contradict each other on it. A bible validated by hand is stillvalidated. Closes #364. - A point the arbiter rejects is now closed even when the rewrite it asked for elsewhere in the same answer is refused. The rejections of a refused answer used to be lost: an error one arbiter accepted and the stronger model then rejected stayed marked as confirmed, and a faithful passage left its book
blockedafter three rounds. Alerts of automatic checks are untouched. Closes #366. - The book export (
GET /api/projects/{id}/export/{format}) no longer answers 422 when the title contains a character outside Latin-1 such as « — » or 第一章 (books created through the API v1 included): the file name now goes in the RFC 5987 formfilename*=UTF-8''…, like the chapter export. Closes #367. - The help and the error under a form field are now announced with it: text inputs, selects and text areas placed in a field point at them with
aria-describedby(after any description the screen already gave), and a field in error is markedaria-invalid. The help still stays out of the field’s name. Closes #341. - A passage translated in parts for a small window is no longer thrown away by its own fidelity check. The check used to rebuild one request with the whole passage and the whole candidate, got
ContextTooLarge, and left the source in place after the translation calls had been paid for. It now falls back to groups of whole paragraphs sized for the judge’s own window (which may differ from the translator’s), half of the room going to the text and the rest to the neighbourhood; a paragraph translated in parts is read part by part, each beside its own translation. The room reserved for the judge’s answer is also computed from its real response format instead of the much larger chapter analysis one, which made the check impossible for any passage in a 16 384-token window. A paragraph that still does not fit, or a group the judge cannot read, leaves the check “not run” as before: no favourable verdict is assumed. Closes #355. - A rewrite is no longer accepted with a new serious error that passed for an old one of the same category in the same paragraph (a second lost negation taken for the first, while another paragraph improved). Two remarks of one category that quote different words are now two defects: the new one counts as added and the paragraph goes back to its previous text. The same defect told in other words still matches, since descriptions are not compared, and a remark that quotes nothing matches by category as before. Closes #359.
- A passage sent to the stronger model for wordplay or verse now stays on it on every rescue path: translation in parts after a context too large, targeted repairs (missing paragraphs, broken markers) and small groups after an invalid answer. These paths went back to the book’s model, with parts sized for the stronger model’s window. When the stronger model is down or refuses the passage, the book’s model takes it over with a context and parts built for its own window, and the autopilot journal records it (
stronger_model/fallback) (#357). - A semantic check that concludes now closes the “inconclusive check” alert an earlier outage of the same stage left on the passage, and drops that outage from the job’s fidelity record. Before, a passage whose check failed once (provider down) then passed stayed “to check”, was still sent to arbitration, and the report counted it as not run. An outage of another stage, the divergences and the unverifiable evidence of the new verdict stay open. Closes #358.
- Resolved alerts in the quality tab no longer fall below WCAG AA contrast: the row’s 65 % opacity is replaced by a sunken background and muted text, two token pairs already checked at 4.5:1 in both themes. The “Resolved” badge still carries the state. Closes #349.
scripts/evaluate_analysis_modes.pyandscripts/benchmark_analysis.pyrun again from a new environment: their throwaway database had no licence, so the analysis stayedpausedand the documented command stopped on anAssertionErrorbefore its report. Both now seed the synthetic signed licence of the test suite (backend/tests/synthetic_licence.py, shared withconftest) after each reset of the database. With several seeds,calls_by_operationis now summed over every seed likecalls(it kept the first seed’s figures). The JSON report of the evaluation changes shape:{"seeds", "aggregation", "modes"}, whereaggregationstates that ratios are the mean of the per-seed ratios and counts the sum over the seeds; the table prints the same line. A smoke test runs both commands. Closes #361.- “Book edition” screen: saving now sends the
revisionread with the edition. When someone else decided a field you change in the meantime, the server refuses (HTTP 409): the message is shown, your input stays on screen and a “Reload edition” button replaces it with the saved version, instead of silently overwriting the other person’s title (#332). - Ordinary provider calls (completions, model list) no longer read a response of any size into memory: the body is read as a stream and held to
PROVIDER_RESPONSE_MAX_MB(8 MB by default) of decompressed data, error bodies included. Past it the connection is closed before the JSON is parsed and the request is recorded as failed with a readable error; an HTTP error keeps its status. Batch ceilings are unchanged. Closes #344. - Codex bridge: the trace database Codex keeps in each account’s folder (
logs_2.sqlite) no longer grows without limit in thecodex-statevolume. Codex 0.160 records every turn there at TRACE level and offers no setting to cap or move it: 4.6 GiB and 3.9 GiB for two accounts on one instance, rewritten continuously and copied in full by every backup. The bridge now empties it before a generation once it passes 64 MiB; Libris never reads it, and the account, its sign-in and running translations are untouched. An oversized file left by an earlier version is reclaimed at the first generation after the update. Closes #371. - On the mobile layout (up to 900 px wide) every button, field, tab, segmented option and menu entry is now a 44 px touch target; small buttons such as “See the report” were 36 px and the main actions 40 px, and at 768 px they kept their desktop size under the mobile menu. Desktop density is unchanged. A menu taller than the screen, such as Export on a phone, now opens on the side with the most room and scrolls instead of hiding its last entries, and no longer runs past the left edge of a narrow screen. A status badge longer than its container no longer widens the page when the text is enlarged. Closes #346.
- Menus take keyboard focus when they open: the first entry is focused once the menu is positioned, so the arrow keys, Home and End move through the entries and Escape closes the menu and returns focus to its button. Focus used to stay on the button, and Arrow Down scrolled the page. Closes #368.
- Library on the mobile layout (up to 900 px wide): the selection checkboxes, book and series titles, the Table/Cards switch, the status sort, the “See a book translated…” link, the logo and the breadcrumb link are now 44 px touch targets. The boxes and the text keep their size; the area that takes the touch grows around them. A checkbox or switch with its label, as in the settings, is 44 px high as well. Desktop is unchanged. Closes #372.
- Mobile layout (up to 900 px wide), outside the library: the glossary’s “Lock”, “Accept” and “Overrides series” checkboxes, the checkbox of each passage to recover, a volume’s selection checkbox in a series and the “I found this batch…” confirmation are now 44 px touch targets. The boxes keep their size and their accessible name; desktop is unchanged. Closes #373.
- Book edition: Escape, the close buttons and a click outside no longer discard unsaved changes. A changed edition asks to save, close without saving or keep editing, and leaving the book asks before discarding it; an unchanged edition closes at once. A failed save keeps the draft, and a slow save no longer replaces what was typed while it was pending.
- Lowering a provider’s concurrency now also bounds the calls already waiting for it: admission compares running requests to the limit currently stored, read under the admission lock, instead of the one loaded before the wait. Calls really running finish; a raised limit applies at once (#338).
- The application opens again in a browser that refuses local storage (site data blocked, or a full quota): the page stayed blank, without even the sign-in form. Theme, language, sidebar and library view now fall back to the system or default setting, and a choice made holds for the session when it cannot be saved. (#339)
- First steps: “test the key” is no longer ticked by a model call that is still running or failed (an HTTP 401 used to tick it). It takes a successful key test or a successful model call on a provider still in use, made since that provider’s key or address was last changed; replacing the key unticks the step until the new one answers (#342).
- The migration that ties quality alerts to a text revision (
5c3e8d1a7b42) no longer loads every alert and its message into memory: fingerprints are computed by batches read on a stable cursor, and the oldest open duplicate is kept by the database. The result is identical. Closes #343. - Import assistant: removing a file can no longer delete the upload that follows it. A removed file freed its number, the next upload took that number and the same file name on disk, and the removal, finishing after its commit, deleted the new file: it was listed as received but missing at confirmation. Each staged file now has its own storage name, never given twice; the numbers shown in the assistant are unchanged, and imports started before the update keep working. No database migration (#335).
- A book no longer ends
blockedon an error the fidelity check disputes. When the arbiter accepts a reviewer’s error and the fidelity check refuses its rewrite of that very paragraph in two separate rounds, each time quoting the source for the text in place, with no other refusal in between, the point is closed on the text in place and listed inresidual_pointswith"motive": "contested_by_fidelity"(the other entries now carry"motive": "open_points"); the book endscompleted_with_residuals. A single refusal, a refusal for another reason (unresolved critique, added error, invalid answer, check without a verdict) or evidence that does not support the text in place still blocks. Refs #366. - A polish kept in part no longer proposes the glossary terms of the whole candidate. When a paragraph went back to its previous text, the terms the model proposed for the refused wording were still submitted, as were its events before 0.27.2; they are now dropped wherever the saved text is not the candidate (polish, revision, quality judge’s correction). A regression test follows the refused event to the next passage’s request, with the narrative rebuild failing and succeeding. Refs #352.
[0.27.2] - 2026-10-04
Upgrade notes
- No database migration, no licence change. The Codex bridge image changes (Codex CLI 0.160.0): the installer pulls it with the application; a bridge left on the former image is reported as a version mismatch in Settings and keeps working with the former model list. The scheduled backup (
deploy/libris-backup) now pauses the API and the worker (docker pause) while it dumps the database and archives the books: translations stop for the length of the backup and resume by themselves.LIBRIS_BACKUP_FREEZE=falseinlibris-backup.confkeeps the former behaviour.
Changed
-
Codex bridge: Codex CLI 0.156.1 → 0.160.0, so that the ChatGPT-account model list offers the models OpenAI has added since, GPT-6.1 Sol (
gpt-6.1-sol) among them. The list is still read from the app-server (model/list): nothing is hard-coded, a provider’s model is chosen in its settings as before. -
A book the autopilot left blocked now offers “Restart the autopilot” as the primary action of its header: one click, no confirmation, starts a new autopilot run. The report stays one click away (“See the report”); with the autopilot off for the book or the series paused, the header still shows “See what blocks it”. Refs #348.
Fixed
- A malformed answer from a provider no longer leaves a request marked
running(#337).choices,choices[0]andmessagethat are null, a string, a list or any other wrong type, as well as an unexpected shape on the Anthropic, Responses and Codex transports, now end as the known protocol error (a few bounded attempts, then a failure the caller already handles). Whatever else goes wrong between admission and recording closes the request with an error status and its duration, which frees its slot and its budget reservation; a pause or a late batch result is handled as before. - The semantic fidelity check no longer sets aside a divergence because some locked term of the paragraph was corrected. The exemption now requires the judge’s citation to be that term and nothing more (the term as the source says it, or a fragment of its own glossary entry): a corrected
sword → glaiveno longer hides “was broken” rendered as “new”, nor a changed cultural reference in the same paragraph. Closes #351. - The four reviewers (translation review, fused review, final review, quality judge) now share one closed list of categories and one definition of
error. A meaning defect a reviewer used to labelgender,referencesorpronouns(so treated as a point of detail) is nowmistranslationorpronoun, and apronounerror (a pronoun, gender or agreement against the source, a decided character sheet or the series) keeps a passage open like a mistranslation. Refs #348. - The autopilot no longer ends
blockedfor points of detail. Only an error that changes what the book says (mistranslation, omission, addition, untranslated text, a locked term) or an error of an automatic check earns a passage another round; from its second reading the final review keeps a new point only on a paragraph whose text changed; and once the rounds are spent, a book isblockedonly for text left in the source, a check without a verdict, an automatic check’s error, or an error the arbiter accepted and could not apply. The other open points are closed on the current text and listed in the report (residual_points, outcomecompleted_with_residuals, exportable). The final review, the fused review and the polish now keep the paragraphs a correction improves and give back only the ones it breaks, instead of refusing it whole. The quality judge’s correction log counts its errors on the corrected paragraphs. Closes #348. - A correction kept in part no longer hands the events of the refused paragraph to the following passages (0.27.1). When a paragraph went back to its previous text, the revision after review and the quality judge’s correction still stored the events and proposed the terms of the whole candidate as those of the saved text: a book could keep “Alice stayed alive” and tell the next passages “Alice died”. The narrative of such a passage is now marked stale and rebuilt from the saved text, and the candidate’s term proposals are dropped. Closes #352.
- A correction kept in part is read again before it is saved, and a moved phrase no longer splits (0.27.1). The text obtained by putting a refused paragraph back is neither the previous text nor the candidate the semantic check read: it now goes through that check again (at most two more readings; still divergent, the correction is refused whole, as before 0.27.1). A paragraph that shares a moved phrase with a refused one goes back with it, in the judge’s correction, the revision and the arbitration alike: put back alone, the refused paragraph said the phrase twice, or the passage lost it. Closes #354.
- The recovery ladder now puts every candidate through the same fidelity check as a first translation before saving it. A translation the check refused, or could not conclude on, used to become the active text one rung later; it is now left out, the ladder goes on to its next rung, and the passage keeps its source (or its earlier translation) when no rung produces a faithful text. Closes #350.
- The help and the error under a form field are now announced with it: text inputs, selects and text areas placed in a field point at them with
aria-describedby(after any description the screen already gave), and a field in error is markedaria-invalid. The help still stays out of the field’s name. Closes #341. - A passage translated in parts for a small window is no longer thrown away by its own fidelity check. The check used to rebuild one request with the whole passage and the whole candidate, got
ContextTooLarge, and left the source in place after the translation calls had been paid for. It now falls back to groups of whole paragraphs sized for the judge’s own window (which may differ from the translator’s), half of the room going to the text and the rest to the neighbourhood; a paragraph translated in parts is read part by part, each beside its own translation. The room reserved for the judge’s answer is also computed from its real response format instead of the much larger chapter analysis one, which made the check impossible for any passage in a 16 384-token window. A paragraph that still does not fit, or a group the judge cannot read, leaves the check “not run” as before: no favourable verdict is assumed. Closes #355. - A rewrite is no longer accepted with a new serious error that passed for an old one of the same category in the same paragraph (a second lost negation taken for the first, while another paragraph improved). Two remarks of one category that quote different words are now two defects: the new one counts as added and the paragraph goes back to its previous text. The same defect told in other words still matches, since descriptions are not compared, and a remark that quotes nothing matches by category as before. Closes #359.
- A passage sent to the stronger model for wordplay or verse now stays on it on every rescue path: translation in parts after a context too large, targeted repairs (missing paragraphs, broken markers) and small groups after an invalid answer. These paths went back to the book’s model, with parts sized for the stronger model’s window. When the stronger model is down or refuses the passage, the book’s model takes it over with a context and parts built for its own window, and the autopilot journal records it (
stronger_model/fallback) (#357). - The translation memory no longer reuses a translation written under another style sheet. A passage of another book is reused only if both books hold the same sheet, series included: same narrative tense, default form of address, honorifics, dialogue, register and profanity, same tu/vous for the couples the passage names, and none of the wordings the sheet replaces. A book set to the present tense took the passé simple translation of an earlier book as it was, and in fast mode no stylistic review corrected it. In the same book, a passage sent back to review by a change of the style sheet is no longer reused until that review settles it. Lines of verse and the same words written as one line of prose no longer share a memory entry, so a three-line poem no longer comes back as one line. The setting that turns the memory off is unchanged. Closes #356.
POST /api/auth/forgotis now limited, in the database so that every API process and every restart share the count: 10 requests per client address in 15 minutes and 30 in a day (IPv6 counted by /64), refused with a 429 andRetry-Afterthat leaves no trace; per account, no new link or mail within 5 minutes of the last one and at most 3 per hour, with the same answer as a sent link whether the account is known or not. Earlier links stay valid. The request counter is separate from the failed-login throttle and nothing is added to process memory by a plain request. Behind a proxy,FORWARDED_ALLOW_IPSmust be the proxy’s address, never*. Closes #331.- Every 429 of the per-client limit of
POST /api/auth/forgotis now logged as aWARNINGwith its bucket (the client address, or the /64 for IPv6) and the wait, and nothing about the account asked for. An operator behind a reverse proxy who readsbucket=172.18.0.1sees thatFORWARDED_ALLOW_IPSis missing. Closes #377. - The scheduled backup (
deploy/libris-backup) no longer reports “written and verified” for a database dump and a books archive taken at different instants. It freezes the API and the worker (docker pause) during both phases, fails and keeps nothing if a file was added, deleted or replaced in the volume meanwhile, and records the hash of every archived file inbooks.sha256, whichlibris-restorechecks after restoring.LIBRIS_BACKUP_FREEZE=falseturns the freeze off. Closes #333. - Secondary status text (progress percentage, estimate, counters, autopilot hint) now meets WCAG AA contrast (4.5:1) on sunken and accent-tinted surfaces in both themes:
--text-subtlegoes from#6a6f79to#5f646ein light and from#8e919ato#969aa3in dark. The token test now covers--text-subtleon--bg,--surface,--surface-sunkenand--accent-soft(#334). - An upload whose inspection ends after the import was confirmed is now refused (HTTP 409, “Cet import est déjà confirmé.”) instead of being added to a session that is already closed: the check is repeated under the session lock, before anything is written. Concurrent uploads to a session still open are all kept (#336).
- Batch mode no longer answers one question with the result paid for another. The durable identity of a batched call only covered the passage’s source text and the user rules: two fidelity checks of different candidates, a check after a term was locked, or the re-examination of a contradictory verdict shared one key, and the second call received the first one’s stored prompt and verdict. The identity now covers every mandatory input of the operation (current, previous and candidate translation, proposals, locked glossary and locked series terms) and the instructions added after the built prompt. Neighbouring passages, the scene’s characters and unlocked series terms stay out of it, so a result is still reused after a restart without a second charge. A batch collected by an earlier version and still pending at upgrade is asked again once. Closes #353.
- Two people editing the edition of the same book no longer undo each other.
PATCH /api/projects/{pid}/editionnow reads the edition under a row lock, so a change of the cover text made from a page loaded before someone else renamed the book keeps the new title: the library title, the stored edition and the EPUB metadata stay the same title.GETandPATCHanswer arevision; a client that sends it back is refused with HTTP 409 when a field it changes was decided by someone else since that revision, instead of overwriting it. Withoutrevision, the last change still wins. The cover file of a replaced artwork is only removed if no later change chose it again. No migration (#332).
[0.27.1] - 2026-10-04
Upgrade notes
- No database migration, no licence change. Built-in rules move to
rules-v13.
Changed
- The quality score no longer charges a passage for its history: a recovery that ended on a translation (10 points) and a critique the arbitration rejected (2 points) are listed at no cost. A recovery that kept the earlier translation, a deferred critique and open points closed without a correction cost what they did.
Fixed
- A correction is no longer refused whole for one paragraph. The quality judge’s correction, the revision after review and the autopilot’s arbitration threw a rewrite of a whole passage away as soon as one paragraph gained an error, or as soon as the semantic check found one divergence: on a one-passage chapter of 41 paragraphs, a correction that took the passage from 12 errors to 3 was refused three rounds in a row and the autopilot ended
blocked. A paragraph the semantic check, the automatic checks or the second reading refuses now keeps its previous text, and the rest of the correction is saved; in the arbitration, the critiques of such a paragraph stay open for the next round (decisiondeferred) instead of being closed. A finding on a paragraph the correction did not touch is no longer held against it. Nothing is kept when no changed paragraph is left, when the passage as a whole is faulted, or when the semantic check did not conclude. A passage whose divergent paragraphs were put back counts aspassedin the delivery report. The polish, the fused review and the final review are unchanged. - Chinese → English: the rules now name literary and wuxia words that mislead (爛銀 is gleaming silver, not tarnished; 心脈 is the heart meridians, not a pulse; 虎口; 殺著) and ask that a material named in the source (古玉) is not dropped for a general word.
[0.27.0] - 2026-10-04
Upgrade notes
- Two database migrations run automatically at start-up (
merge_repairs, thenquality_issue_revisions, a single chain). The second adds to each quality alert the revision it was checked on; existing alerts have no known revision and becomerecheck: still open and listed for review, but they no longer cost points until a later check finds them again. No licence change: compatible with the deployed licence server 0.13.0.
Changed
-
The fidelity auditor prompt now takes TARGET_TEXT and the mandatory choices (confirmed identities, human decisions, locked glossary, series conventions) as its only reference:
PREVIOUS_TRANSLATIONonly says which units changed, is never quoted as evidence, and a candidate that corrects it by agreeing with the source or a locked term is not a divergence. The judge fillssupported_readingfor every divergence it reports. The fidelity corpus gains acorrectionkind (feelers/antennes with a locked term, and four “actually more faithful” cases, each in fr, en, zh and ja as target) and a negative witness (locked term corrected and number changed: still a divergence); its tests check the expected answers only, never an observed one. Built-in prompts move tofile-v30(#326). -
The two unreleased migrations (
merge_repairs,quality_issue_revisions) now form a single chain:alembic upgrade headno longer fails on two heads -
A change to the style sheet (book or series), to the instructions of the book, of a chapter or of a passage now sends the translated passages nobody validated back to review, with the reason (“The book’s style sheet has changed.”), as a glossary or wording change already did; saving the same value again changes nothing.
GET /api/projects/{id}/styleand therule_originsof the context inspector say where each rule the translation follows comes from (volume, series, analysis, none), andGET /api/projects/{id}/glossary/effectivemarks withconflictan unlocked series decision set against an unlocked book term (the series’ is kept, nobody confirmed it) (#329). -
The real-engine evaluation bench (
python -m app.eval.run) now freezes its non-regression criteria (backend/eval/criteres.yaml) in the manifest before the first paid call, andcompareapplies them: a rise of missed defects, false alarms or undetermined verdicts in any one dimension, a drop in delivered text or a cost increase beyond the tolerance exits with code 1; runs made under different criteria, or stopped runs, are not compared. A run names its model configuration (--configuration, two documented:luna,luna-sol) and may use a separate annotation judge (EVAL_JUDGE_PROVIDER_ID). It refuses to start in CI or on an installation with jobs under way, stops when another worker takes its job, and always leaves a partialreport.jsonwhen it stops.EVAL_COMMITnames the commit where git is absent (#330). -
The chapter analysis now quotes the proof of each alias:
aliases_evidenceholds, for every alias a character is given, the exact sentence of the passage that names both names as one person (a quote that is not in the passage is dropped; the aliases themselves are kept). An alias is declared only when the text attributes it to the same person; otherwise it goes toproposed_aliases, and two characters who appear together, a title or a common noun are not aliases. An older analysis without the field still reads. The merge itself does not use it yet. Promptsfile-v27(#324). -
A character profile that lists another sheet’s name among its aliases no longer becomes that sheet on the model’s word alone: two unvalidated sheets are merged, or a profile attached to a single sheet, only when a quote of the passage (
aliases_evidence) names both and nothing forbids it. Two known genders that differ, a generic name (“boss”, “the bandit leader”) or one two sheets share, an active relation between the two, or the analysis of one passage describing them as two characters keep them apart whatever the quote. A refused alias goes to the proposed aliases, where it shows as a merge suggestion; the sheet records the refusal (kept_apart: name, reason, passage), which holds for every later automatic decision until a person merges the two. An accepted merge quotes the passage and the sentence in its reason. A real pseudonym with no sentence naming both names is therefore a proposal to confirm, not a merge. Validated sheets and a person’s own lists are decided as before; sheets already merged are not repaired; the memory of earlier passages and the series bible follow separately (#324). -
The memory of earlier passages rebuilt by the parallel analysis and the series bible now follow the rule of proof of declared aliases. Replayed in book order, an analysis that lists another identity’s name among its aliases joins it only on a quote (
aliases_evidence) naming both, and never across differing genders, a generic or shared name, a relation between the two, two characters of one passage or an earlier refusal: an analysis stored without quotes no longer merges two identities. A character’s dated record never goes to a sheet it was kept apart from. In a series, a volume’s sheet joins a series identity known under another name only when a quote kept on a linked sheet (alias_quotes, the latest 20) names both names; otherwise the link is a proposal to confirm. A name noted inkept_apart, or the name of another series identity, is never added to a series identity’s aliases, and a refusal removes it if an earlier refresh had added it. Links a person made are kept. Sheets analysed before this version carry no quote: their existing links stay, new ones under another name are proposed (#324). -
The database’s
max_connectionsis nowPOSTGRES_MAX_CONNECTIONSin.env(200 by default, unchanged), and the newWORKER_PROCESSESsetting (1 to 16, default 1) says how many worker processes share it. At start-up the API and the worker compare PostgreSQL’smax_connectionswith(1 + worker processes) x (DB_POOL_SIZE + DB_POOL_MAX_OVERFLOW) + 20and logdb_pool=over_budgetwith the value to set when it is too low; they still start. Nothing changes with one worker (#322). -
The tu/vous couples of a book’s style rules are written one line per form instead of one sentence per couple: 100 couples cost about 1,000 tokens instead of 19,256 in every prompt. The strong sentence stays beside the passage (
ADDRESS_FORMS) for the couples it features. When more than 100 couples are read, the cut keeps the most read, not the first read. Prompt rulesrules-v11(#321).
Added
-
Narrative context API for other applications of the ecosystem (Libris Draw):
GET /api/v1/books,GET /api/v1/books/{id}/narrative-context,GET /api/v1/books/{id}/narrative-context/revisionandGET /api/v1/series/{id}/narrative-contextanswer a versioned snapshot of a book — chapters, passages with their source and translation in force, Book Bible, character sheets with aliases, mentions and sourced observations, relations, places and objects, glossary, events — with Libris’ stable identifiers, aprovenanceon every item, and fingerprints (revision,content_revision, one per chapter,checksum) that tell a client whether its snapshot is still current. Read only, under the new token scopenarrative:read; no database migration. -
A wrong identity merge can be repaired, one absorbed sheet at a time (#325).
GET /api/projects/{pid}/characters/merges/{mid}/repair?source_id=previews it without writing anything: the chain of merges read in the journal (A → B → C), the sheet given back from its snapshot, the names, fields, list entries and relations that return to it, those that stay, and those nobody can attribute.POSTon the same path applies that preview in one transaction (plan_token; 409 when the memory changed since, 409 with its reason when the journal lacks the snapshot), records the report on the merge (repairs, exported with the book) and in the audit journal, and returns the stored report when replayed. What a person decided, validated or added after the merge is never taken away; an element without provenance stays where it is, listed for a person to settle (decisions:source,targetorboth), never attributed by guess. New merges record the relations as they stood, so they come back exactly; older merges only get back the relations the passage analysis attributes. Both sheets become confirmed identities, so a later analysis proposes but no longer merges them; unvalidated translated passages naming either are marked to recheck, no translation is rewritten, and a series identity still shared by the two is reported, not changed. The screen follows separately. -
The merge history of the Characters screen is now a readable list, with a “Repair” button for each absorbed profile. The window shows the preview before anything is written: what goes back to the restored profile, what stays on the current one and why, the passages that will be rechecked, and a choice (restored profile, current profile, both, or left as it is) for each item nobody can attribute. A refusal of the server is shown with its reason, and a repaired profile is marked “Repaired” (#325).
-
The evaluation bench measures the reliability of diagnoses and accepted corrections against the micro-case references (
app.eval.reliability): TP/FP/TN/FN with denominators, right corrections accepted or rejected, harmful corrections accepted, undetermined outcomes kept apart (never a success), every result tied to its source, candidate, diagnosis, decision and recorded text, with cost, calls, tokens, duration, retries and escalations, and a 95 % interval flagging small samples. No model call; runs in CI (#330). -
A reference set of 48 synthetic CC0 micro-cases (
backend/eval/micro/, six difficulty categories, six language pairs) for measuring false diagnoses and the fate of proposed corrections: each case holds a source, an initial translation, an optional candidate, the context and a justified reference written before any run, with correct witnesses and the disagreements of a blind second annotation kept visible.read_microvalidates it without a model call and refuses a real run on a set that was not reviewed or was edited after its review (#330). -
WORKER_PROCESSES(default 1, unchanged) lets the worker container run the books in several processes, one CPU core each: with N ≥ 2 the worker keeps every periodic loop (licence, watched sources, retention, mail, memory) and starts N job processes that share the queue through its existing leases and locks. A process leaves the next book to the others once it holds more than its share, a dead job process is started again and its books are taken over when their lease expires, and a stop is passed on and waited for within the 30-second grace period. One book alone still uses one core; run a single worker container; each process has its own database pool (#322). -
When the Book Bible synthesis is too large for its provider, the “unconsolidated sections” warning of a book now names the provider used, its context window and output reserve, and the tokens needed against those available; it lists the configured providers whose window is large enough (or the window to configure, noting that a provider set to 100,000 tokens is enough when it is), with a button to the book settings (#319).
Fixed
-
The autopilot no longer locks a descriptive name that other characters also carry: 黑衣人 (“man in black”) was locked as a proper name (“Black-Clad Rider”) although two other characters are named 持銅錘的黑衣人 and 使鏈子鏢的黑衣人, so every translation of the chapter was refused (“Locked translation missing”) through each recovery step and both models. Such a term stays in the glossary, accepted and unlocked.
-
A quality alert now knows the text it was found on (#328). Each carries the revision of the passage and of the book’s memory it was checked on, its origin, and a state:
active(found on the current text),recheck(the text or the memory changed since, or the revision is unknown: still open and still listed for review, but no longer costing points),resolved(a later check no longer finds it),rejected(judged wrong by a person or the arbitration) orhistorical(closed without proof). Nothing is deleted any more: a corrected passage keeps the history of what was fixed, an unfixed defect is confirmed in place instead of being written again, and an alert set aside by a person is not reopened by the next global check. The database refuses two open alerts for the same defect (34 exact duplicates had piled up) and a closed alert without a reason; a result computed on a revision the passage has left is ignored. A change of memory alone sends the open alerts torecheckwithout closing any. The score, the open-alert counters (issues_active,issues_recheckin the completion report) and the alert list now agree;GET /projects/{id}/issueslists the open alerts withstate,origin,segment_revision,memory_revisionandcurrent, and?history=trueadds the closed ones. Existing alerts keep their rows: the migration marks themlegacywith an unknown revision (open onesrecheck, closed oneshistorical, exact duplicates of an open onehistorical), the worker re-runs the free automatic checks on them without any model call, and project archives carry the new fields (an older archive restores its alerts aslegacy). Scores may rise after the upgrade, since alerts not proven on the current text no longer count. -
Every step that writes, reviews or judges a translation now follows the same order of rules, not the translator alone: the book’s explicit rules and style sheet (the volume’s, then its series’), a person’s decisions, the locked glossary, the locked series terms, then validated facts, memory and inference. The Book Bible sent with a passage says whether a person validated it (
status:validatedorinferred) and no longer repeats what the style sheet decides (tense, honorifics, dialogue convention), so a review or an arbitration no longer brings back the tense the analysis had inferred. The consistency check builds its rules with the same code as a passage’s context. Promptsfile-v28, rulesrules-v12(#329). -
A review diagnostic nothing supports is set aside before it feeds a correction, an arbitration or a count of open points. The review, the quality judge, the final review, the book-wide consistency check and the arbitration now qualify three cases without a model, in any language: a unit the passage does not have, words quoted in support of a replacement and found nowhere in the passage, and a replacement identical to the current text (”?” replaced by ”?”; spacing and typography aside, case kept). Units are read with their passage, so a remark on a unit reduced to a sign or a tag is neither always kept nor always dropped, and a warning with no replacement stays possible. The arbiter is told that a justification is a claim to verify and marks a rejected proposal
missing_evidenceorcontradicted_by_context(a reasoning the source in context contradicts, even with an exact quotation). Each diagnostic set aside is written to the decision log with its motive, closed, and not asked again. Consistency remarks and critiques stored by earlier versions are screened when the arbitration reads them. Built-in prompts move tofile-v29(#327). -
A correction is no longer refused by the very evidence that proves it right (#326). The semantic fidelity check took any divergence whose quote existed in the source or in a mandatory choice as a refusal, so a revision that put a locked term right (“feelers” → “antennes” in place of “palpitations”) was vetoed by the quote “feelers” and the mistranslation stayed. The server now sets such a divergence aside when the candidate says the locked translation of a term of the source unit and the previous text did not (categories object identity and named technique only): the correction is saved, the check is recorded
passedwith reasoncontradicted_by_evidence, and a warning review point keeps the judge’s description. Any other verified divergence of the same unit (negation, number, agent, injury, result) still refuses the candidate, and a quote held only by a human decision is never set aside. The judge now states which version its quote supports (supported_reading); when it refuses a candidate on evidence it says supports it, it is asked again once (two calls at most): the second verdict decides (reexamined), and a verdict still contradictory, like an unavailable provider, isnot_runand keeps the previous text. -
A volume of a series waiting for an earlier volume is no longer started every 15 seconds only to wait again: the queue checks the earlier volume without claiming the job, so its attempts, its log lines and its status no longer move until it can really go on. One such job had reached 8,492 attempts in two days (#323).
-
A book whose autopilot report is
blocked, or an oldercompleted_with_residualsreport that still leaves open passages, is now shown as not ready on its page: the report is recounted and blocks the book, and the main action stays “See what blocks it” even when the autopilot is switched off for the book (#320). -
The final review no longer fails with
ContextTooLargeon a passage carrying hundreds of consistency remarks, even with a 100,000-token window (#321): its earlier checks are capped at 20 distinct lines and 6,000 tokens, errors first, with a “+N autres” count, and its earlier critiques at 6,000 tokens, in the full and the reduced context alike. A consistency remark written again stays one line, a passage keeps at most 5 open ones, and a recheck at a new revision resolves the remarks written before it. -
A Book Bible too large to take even one more section no longer blocks the whole analysis: the new sections are synthesised as a separate batch with the same provider, then joined to the bible without losing a fact of either (lists merged, both summaries kept). A volume whose bible overflowed the input left by its provider by a few percent now ends with every section consolidated and goes on to translation instead of stopping on
analysis_unconsolidated(#319). -
The automatic re-review that follows a changed decision now ends with its own autopilot report, recounted against the passages as it leaves them; it was the book’s last job and carried none, so DxD volumes 21 to 23 looked finished without a report (#320).
[0.26.0] - 2026-10-02
Upgrade notes
- No database migration. The licence certificate may now carry an optional
updates_untildate; certificates without it and perpetual licences remain unlimited. Compatible with the deployed licence server 0.12.3.
Added
-
Tamil (
ta) is listed among the source and target languages of the import wizard and the book settings (#313). -
Tamil (
ta) as a source and target language (libris/libris#313): listed byGET /api/languages, with its typography for the prompt (Latin punctuation without a space before it, “ ” quotes; rulesrules-v10), its script for the untranslated-text check, and no French or Spanish rule applied. Word boundaries now treat every Unicode mark and the ZWNJ/ZWJ joiners as letters of the word (LIB-664), so a glossary term, a name or a locked term is no longer found inside a longer Tamil or Hindi word (மன் in ராமன், राम in रामू); accent folding drops accents but never an Indic sign (கண் is not கண). A Tamil name is recognised with its case ending in the source and in a locked translation (ராமன் → ராமனுக்கு). The quota counts Tamil words by spaces; an English → Tamil length ratio (about 0.9 to 1.4) stays inside the existing bands. -
A book is no longer shown as “Complete” when it is not ready: the server computes a
display_state(blockedwhen the autopilot ended blocked,to_checkwhile passages stay in check, error or refused or the report kept residuals,completedonly without either), distinct fromprojects.status, which is unchanged. It comes with the books and the volumes of a series (display_state, also inprogress),GET /api/v1/series/{id}and the MCP book list (#305). -
New versions of a Personnelle licence (phase A of libris/libris-licence#46): the image carries the date of the commit it was built from (
app.released, written by the build fromCI_COMMIT_TIMESTAMP, never read from the environment; a development image has none and is never outside a period). The certificate may carryupdates_until(Unix seconds); absent,null, 0, not a number or a perpetualv: 2certificate mean no limit, and a v1 licence server needs no change. Settings › Licence shows “New versions until” andGET /api/settings/licencegivesupdates_until,released_atandcovered; the update banner warns, without hiding the install command, that an announced release may not be included once that date has passed. Nothing is blocked yet: translation and the installer check come in phase B. -
Read-only demo interface (DEMO_MODE): when
GET /healthanswersdemo: true, a “Read-only demo” banner with a link to the free trial shows on the sign-in page and every screen; importing, launching an analysis or a translation, book edition, publishing, WebDAV, archiving, shared glossary changes and the account screen are hidden, while reading and exports stay available. Any other write is stopped before it reaches the server with the same explanation (#282). -
The read-only demo also hides the write controls left in the editor (translations shown read-only, no save, validation, retranslation, instructions or passage inspector edits) and shows the Book Bible, Characters, Glossary and Settings tabs of a book, the series pages and the shared glossary detail read-only (fields not editable, no “Add a term”), as well as the queue priorities. The server keeps refusing every write (#290).
-
In the read-only demo, selecting books in the library or volumes of a series only offers “Export EPUBs” (no Configure, Analyze, Translate or menu), and a book without a provider no longer shows the provider setup banner (#292).
-
First-launch list on the library for an administrator: activate the licence, add a provider, test its key, import a first book with its estimate, change the initial admin password. Each step ticks itself off from
GET /api/onboarding, links to its screen (#settings/licence,#settings/providers, the import wizard, the account) and the list disappears once everything is done or hidden; it blocks nothing. Settings › LLM providers offers OpenAI, Anthropic, OpenRouter and local server templates for a new provider and warns when no price is entered, since estimates would then show €0. The list links to the showcase of a book translated end to end on libris-translate.com (#283). -
On a demo instance whose
GET /healthanswersdemo_login: true, a visitor without a session enters the demo at once (POST /api/auth/demo); after signing out, the sign-in page offers an “Enter the demo” button instead of signing in again on its own (#294). -
Demo operator guide: enable read-only mode, load showcase books before activation, and reset from project archives; restored books count toward the licence quota.
Changed
-
The cost estimate no longer mixes target languages (libris/libris#314): the history of the owner’s books only serves books written in the same target language (
fr-FRandfrare one), so French books no longer set the estimate of a Tamil one, and a Tamil book is first estimated from defaults calibrated on the measured English → Tamil run of LIB-670 (3.7 times the calls, 1.1 times the input and 1.4 times the output tokens of a call: about 3.3 M input and 240 k output tokens for 3 700 words, against 0.8 M and 47 k before). The import estimate (GET /api/imports/{id}/estimate) takes an optionaltarget_language, which defaults to the target volume’s; without one it keeps the whole history and the average defaults. -
The pairs of characters whose forms of address (tu/vous) the passage speaks of are restated in an
ADDRESS_FORMSsection right before the passage, in every request but the analyses. One pair among thirty inUSER_RULESwas read as one more line: on the Pigeon passage of Alice the model saidvousin 4 replays out of 4 against atupair, and 0 out of 4 with the pair restated beside the text (LIB-625). -
Verse keeps its lines, its rhyme and its invented words (libris/libris LIB-622): a passage with at least three short lines (a poem, a song, a refrain, a Jabberwocky) is translated by the stronger model when one is set, with a
VERSEsection that asks for the source’s rhyme scheme and meter, one line for one line, and a recreated invented word that is coined once, reused everywhere (new_terms) and never left half in the source language. Every correction and review prompt now also says that verse is corrected and judged as verse (rulesrules-v9). The journal counts these upgrades (verse). -
Spanish machine output no longer leaves a space after the opening ¿ and ¡ (« ¿ Te gustarían…? » becomes « ¿Te gustarían…? », inline markers included): Libris applies it on every machine write and on every output, like French typography. Nothing else changes in Spanish, and the sentence telling reviewers that Libris sets the typography stays French-only (#311).
-
The translation check now reports a competing form of a translated name (libris/libris#302): the noun alone no longer passes when another capitalised word follows it (« Lapin Rose » for « Lapin Blanc »), and a Spanish, Italian or Portuguese name only agrees in gender (« Loro » is not « Lori »). Tests cover White Rabbit, Mouse, Duck, Lory and Dodo in French and Spanish.
-
Descriptive character names are translated by default (libris/libris#302): a name made of common nouns or adjectives (the White Rabbit, the Mouse, the Duck, the Queen of Hearts) is proposed in its target form by the chapter analysis (French: Lapin Blanc, Souris, Canard, Reine de Cœur; Spanish: Conejo Blanco, Ratón, Pato, Reina de Corazones), one form per character; only a proper name (Alice, Dinah) keeps its form, and the Book Bible’s
name_policynow concerns proper names only. The autopilot locks the form the analysis gave whatever the policy, even without one, and no longer freezes a name-title in English from the characters list. The translation check accepts the noun alone for a two-word name (« Lapin » for « Lapin Blanc »; « Reine » for « Reine de Cœur ») and reports the source form left in a translated name (« White Rabbit », « Rabbit »). Built-in prompts move tofile-v25. -
The first-book guide now starts with license activation.
-
A reasoning provider that answers only once
reasoning_effortfalls back tonone(Qwen under vLLM ignoringminimal) is now sentnonedirectly on later calls, per provider, model and configured effort, instead of spending the whole output budget reasoning on every request (#277). -
The provider test explains a model that spends its output budget on reasoning without answering, read from its last calls (
reasoning): how many of the last requests stopped on reasoning without content nearmax_output_tokens, the reasoning level sent, the advice to set it to Disabled (reasoning_effort=none), and a button that does so in the form (#277). -
The provider test (
POST /api/providers/{id}/test) now says when the last calls ended on reasoning alone close tomax_output_tokenswith no content (a server that ignores the requested effort, such as Qwen under vLLM): newreasoningfield and an explicit message, read from the calls already logged, tolerant of purged or missing usage details (#277). -
The chapter analysis no longer rejects a whole answer for a variant of wording: an event
kinda model writes asfact(oraction,events…) is read asworld_fact(event…) and an unknown one as a plain event; a character’srelationships,translation_notesand their evidence sent as one string become a one-line list. A wrong structure (an object, a number), the gender, the pronouns and the aliases are still refused (#278). -
Forms of address stay the same between two characters for the whole book: the chapter analysis names, with a quotation, whether each pair speaking in a passage says tu or vous (tú/usted, du/Sie); the first reading holds, and every passage, its review and the final review read it as a style sheet rule. A pair decided in the style sheet (volume or series) always wins over the analysis; a forced analysis forgets what the previous one read. Spanish targets get one plural address: ustedes only for
esandes-419, vosotros fores-ES. Both reviews report a line that changes a pair’s form of address or leaves the narrative tense of the sheet as an error. Promptsfile-v19, rulesrules-v8(#273). -
The forms of address read by the analysis are one vote per reading: a couple is one line whatever name it was read under (canonical name, alias or translated name of the character sheet, in either order, e.g.
Cheshire Cat/Chat du Cheshire), and the form most read for it holds instead of the first one, so a single noisy reading no longer decides the whole book. On a tie the form read first holds, the pair is markedtieinProject.configand logged. A person’s pair in the style sheet also wins over an analysis reading that names the same character differently (#306). -
The pair rules of the style sheet now say that the form holds in both directions and in every line the two characters speak, never once the other form (#303). Measured on the M2 French extract with the pair in the sheet: the translator still slipped a « vous » to the Mouse in 3 of 6 runs with the former wording, in 0 of 5 with this one.
Added
-
Book edition: a per-book switch for the machine-readable “AI-assisted translation” notice of exports, on by default, with a sentence explaining it;
GET/PATCH /api/projects/{id}/editionexpose it asai_disclosure(effective value), withai_disclosure_defaultandai_disclosure_book(the book’s own choice, null = follows the installation) (#284). -
Every EPUB Libris exports (translated, bilingual and reading copies) now says in its metadata that the translation was AI-assisted: a
dc:contributorwith the translator roletrl(“Libris Translate (traduction assistée par IA)”),libris:ai-assisted-translationand the Libris version (libris:version). EPUB 3 declares thelibris:prefix on the package; EPUB 2 usesname/contentmetas. The builders takedisclosure=Falseto leave it out; the notice a source EPUB already carried is replaced, never duplicated. The unusedcreditparameter of the EPUB rebuild is removed (#284). -
DEMO_MODE=truemakes a read-only demonstration instance: every write through the interface,/api/v1and/mcpis refused with 403demo_read_only, except signing in and out (second factor included) and the grouped EPUB and text exports; SSO, LDAP and/mcpare closed; the worker starts no job and no scheduled polling, only the licence heartbeat;/healthanswersdemofor the interface (#282). -
DEMO_USERNAME(withDEMO_MODE) signs visitors into that account without a password throughPOST /api/auth/demo;/healthanswersdemo_login. The account must exist, be active, local, without a second step and not an administrator, otherwise 503demo_unavailable(never created); an existing session is kept, a demo session lasts 2 hours at most, has no recent sign-in proof, and at most 200 are kept; one address opens 20 at most per 5 minutes (429), and/api/auth/meno longer records the interface language of a demo account. An administrator demo account is reported at startup (demo_account_is_admin) (#294). -
First-launch checklist API:
GET /api/onboardingtells an administrator which first steps are done (licence, provider, key test, first book, initialadminpassword changed) and whether any provider has a price;PUT /api/onboardinghides or shows the list again. Everything is read locally, nothing blocks the application (#283). -
The machine-readable “AI-assisted translation” notice of exported EPUBs is on by default and can be turned off for the whole installation (
PUT /api/settings/exports) or for one book (ai_disclosureofPATCH /api/projects/{pid}/edition, the book’s choice winning); the choice applies to the download, batch, API v1, library, WebDAV and e-mail paths alike, with no schema change (#284). -
The provider form shows the input left per call (window − output − response format reserve, the reserve sent by
GET /api/providers/policyasinput_budget) and warns below 48,000 tokens, the usual Book Bible synthesis of a long series: under it the synthesis no longer fits in one call. The figures follow the window and output as they are typed (#281). -
Wordplay kept in translation: the passage analysis now lists the puns, deliberate distortions and lexical misunderstandings of each passage (the exact source words and their effect); only those passages are translated or revised by the book’s stronger model, told to rebuild the effect in the target language or to note what was lost. Each upgrade is written once per passage in the autopilot journal (
kindwordplay), so the stronger model’s calls stay bounded and countable; without a stronger model the book’s model gets the same rule. Built-in prompts move tofile-v17(#274).
Fixed
-
A translation left in the Latin alphabet for a target written in its own script (Tamil, Russian, Japanese, Arabic…) now raises the
untranslatederror: below 20 % of its letters in the target script, from 20 letters on, so that a name, a chapter number or a passage quoting a few English words is never flagged (#317). -
The import wizard asks for its cost estimate with the chosen target language (
target_language) and asks again when it changes, so a book into Tamil is no longer priced with the average defaults (#314). -
An exported EPUB whose source navigation document has no
<head>is no longer rejected by EPUBCheck (RSC-005,OEBPS/nav.xhtml): the rebuild adds a<head>with a<title>before the<body>(#316). -
A fuller name the text never says (« Alice Liddel », read from the publisher’s blurb) is no longer locked as the rendering of its alias (« Alice »): a glossary name whose translation only adds words to the source is another name of the character, not a translation, and is locked in its source form (LIB-619).
-
A form of address read in the source is no longer dropped when its quoted sentence holds an italic word (LIB-621): the citation check ignores the inline markers (
⟦t0⟧…⟦/t0⟧), so the Alice/Caterpillar pair reaches the style sheet instead of leaving the defaultvousto the model. The two review prompts (file-v26) also state that a pair names two characters only and that a couple without a pair is judged on consistency, not on a form the reviewer assumes: on one passage, 3 of 8 replays flooded the review with 6 to 10 false register errors, 2 minor ones remain. -
French typography (#310): the space a model leaves after an elision is removed (
qu’ est-ce→qu’est-ce, alsol’,jusqu’,aujourd’…), at save and at export. Alice M3 delivered “qu’ est-ce qu’une course collective fantaisiste ?”. A closing ‘…’ quote keeps its space. -
Under the
keepname policy, a character the analysis translated in another form (« old Crab » → « Cangrejo viejo », said too rarely to stay) is no longer locked untranslated in the glossary (Crab → Crab); proper names (Alice, Dinah) are still kept (#308). -
A name the autopilot locks in the glossary no longer carries the target language’s article:
Queen→ “la Reine” is locked as “Reine”,King→ “el Rey” as “Rey”; the article follows the sentence (“de la Reine”, “del Rey”). A capitalised article belonging to the name (La Fontaine) stays (#309). -
Library and series page: a volume’s pill follows the server’s
display_state: “Action required” (red) when the autopilot ended blocked, “Points to check” (amber) while passages stay open, “Complete” only without residuals; the library’s sort by state and its “Needs attention” filter follow it too (#305). -
Chapter analysis: a form of address, a relationship, a referent or a character attribute whose quote spans a hard line break of the source (Standard Ebooks/Gutenberg EPUBs break lines in mid-sentence) is no longer dropped because the model quoted it on one line; only spacing is forgiven, an invented quote is still rejected. In Alice, the Alice/Mouse and Alice/Lory pairs now reach the style sheet (refs #303).
-
Quality checks: a closing « » » with no opening one in a translated paragraph (Spanish, French, Italian, Portuguese, Catalan, Russian…) is reported (
orphan_quote, warning) when the source paragraph is balanced, so that review and the autopilot fix it. An Alice reply opened with a dash ended with an orphan « » » nothing reported. A « » » resuming a quotation at the start of a paragraph or stanza is not reported (#307). -
EPUB import (segmentation 4): a long run of loose text (paragraphs set straight in
<body>, separated by blank lines) is cut between its paragraphs before its sentences. In the Alice test edition, “However, everything is queer to-day.” opened the next passage alone: the translator closed the quote too early and the line read as an addition without source. Books imported before keep their cut, so their archives restore unchanged (#304). -
Typography (#301): the EPUB page
<title>and the table of contents copied a translated chapter heading with ordinary spaces where the heading had no-break spaces (Chapitre 1 : …before the colon); they now keep them. A regression test also locks that the French typesetting and the EPUB rebuild never add a space after an apostrophe or an opening ¿/¡/« before an inline code (l’<i>écoute</i>,Qu’<i>est</i>-ce,¿<i>Qué</i>). The exported EPUBs were already correct; the spaces seen in the M2 grading came from a text extraction that joined XHTML nodes with spaces. -
Semantic fidelity check: the judge’s evidence is compared after normalisation (NFKC, non-breaking spaces as spaces,
⟦…⟧markers removed, repeated spaces collapsed) and accepted when it is a substring of the source unit. The judge only namesunit_idand a short source quote; the server quotes the previous and candidate unit texts. A divergence whose evidence cannot be verified no longer discards the whole verdict (invalid_evidence, previous text kept): verified divergences still reject the candidate, the others become a warning review point “unverifiable evidence” without blocking (#300). -
Update EPUBCheck’s Jackson components to 2.18.11, fixing CVE-2026-91776 and CVE-2026-91777 (HIGH in
jackson-databind) reported by the container vulnerability scan (libris/libris#298). -
A character’s name in a chapter title is no longer reported as wordplay (libris/libris#295): “Bill” in “Chapter 4: The Rabbit Sends in a Little Bill” was read as a bill (beak, invoice) in both the extraction and the reconciliation, and the false play sent the passage to the stronger model. The three analysis prompts (
file-v24) now leave a name out unless the text plays on the common word, and the grounding drops a pun on the title line whose second reading is the same word (empty target, or the same word in another case); tale/tail on the title line is kept. -
Book Bible name locking: a name is counted only where it is written in its capitals, so the common noun (“a duck”, “the duck”) no longer swells the count of “Duck” and a name used rarely is no longer locked by mistake (#296).
-
A Book Bible synthesis whose answer runs out of the provider’s output budget (
finish_reason: length, JSON cut off) is no longer lost whole: its input is split in halves, at most two levels deep, and only that synthesis is asked again; the other syntheses are not repeated. Past that bound the synthesis is skipped as before (book_biblein the autopilot journal). Other invalid answers are not split (#277). -
A long series no longer starves the provider behind it: preparing a passage built the URI of each event of the series by reading the OpenViking settings again (a query and a key decryption per event, ~2 000 times for one passage of a twelve-volume series, more than half of its CPU), then read the retrieved files one after the other. The namespace root is read once and the files are read together. On a twelve-volume synthetic series with a 150 ms OpenViking and a Qwen provider of 10 places, a passage is prepared in 0.7 s instead of 3.1 s (0.55 s of CPU instead of 1.8 s), the model receives 9.0 requests at once on average instead of 3.3, and the worker uses 34 % of a core. The job journal now says, at each start of a passage, the delay since the previous one, the effective width (
job_parallelism) and the passages in flight (parallel=start gap=… width=… in_flight=…) (#293). -
A Book Bible synthesis still too large for the provider once split by structure (new chapters joining an existing bible, or a merge of the tree) is now rebuilt from the syntheses of its two halves of chapters, merged in turn, down to single chapters, before the book is blocked; the explicit
book_bible_context_too_largestop remains when even that does not fit (#281). -
A chapter title on the first line of a TXT or DOCX passage, merged with the first paragraph, is now examined for wordplay like any other proposal: the three analysis prompts say that when TARGET_TEXT opens with a chapter title its play counts and its source is the title line. Measured on Luna on the merged « Chapter 3: A Caucus-Race and a Long Tale » passage, tale/tail is listed in 3 extraction replays out of 4 (1 of 4 before) and in 3 reconciliation replays out of 3 (1 of 3 before). The segmentation is unchanged. Built-in prompts move to
file-v23(#291).
Tests
- Check that every autopilot rewrite refusal reason has an English translation (#268).
- Synchronize the phone-sized bilingual export test with the mocked session and project responses before checking the page.
- The provider reasoning-budget test replaces
llm.modelson the instance: after another test’s monkeypatch, an instance attribute hid the class-level replacement and the test really calledhttps://1.1.1.1/v1/models(HTTP 301 onbackend-postgres, pipeline 4172; #318).
Fixed
- Parallel analysis (
ANALYSIS_MODE=parallel, the default) now learns the forms of address per pair of characters: the extraction and reconciliation prompts ask for them, and a reconciliation that leaves them out keeps the extraction’s. Promptsfile-v22(#279). - A pair of words pronounced alike that a qualifier fits both ways (« A Caucus-Race and a Long Tale ») is now listed as wordplay even in a chapter title: the extraction had noted tale/tail in
style_notesonly, and the reconciliation then dropped the play, so the title went to the book’s model with no note. The wording is in the three analysis prompts; measured on Luna, the title now yields the play in 3 replays out of 4 (0 of 5 before) and the reconciliation keeps an extracted play (2 of 2, 0 of 2 before), with no play added to the five control passages. Built-in prompts move tofile-v21(#280). - TXT and Markdown exports of an EPUB chapter laid straight into
<body>as plain text (paragraphs separated by blank lines only) keep each paragraph on its own line, separated by a blank line, as the EPUB export does; before, the whole chapter came out on one line (#276). - Wordplay found by the analysis is no longer lost when the model’s quote drifts: each play now names the one source word it turns on (
word) and the word it plays on (target, e.g. tale → tail), and is kept when that word is in the passage even if the quote carries quotation marks or an ellipsis. The analysis prompts define wordplay by the form of the source words (double meaning, sound-alike, distortion, lexical misunderstanding); the rule sent with a flagged passage keeps a deliberately wrong word visibly wrong in the target language and asks for awordplay:translator’s note when no equivalent exists. Built-in prompts move tofile-v18(#274). - Character names follow the Book Bible’s name policy: the book analysis now says whether names and name-titles (the White Rabbit, the Duck) are kept or translated (
name_policy), the chapter analysis proposes them asnameterms, and the autopilot locks every accepted name used at least as often asAUTOPILOT_GLOSSARY_MIN_CONFIDENCErequires, in its source form when names are kept; a name-title said without its article (« Duck said ») counts too, and a later analysis batch with no policy keeps the one already decided. A term a person wrote or corrected is never rewritten nor locked. A locked name now also keeps its capitals in the translation check (« el Pato », not « el pato »); other locked terms keep ignoring case. Built-in prompts move tofile-v20(#275). - Names kept by the Book Bible are locked even when the analysis lists them only as characters, not as glossary terms: under
keep, each character the book names often enough enters the glossary in its source form and is locked; a term a person decided stays untouched. On Alice, GPT-6 Luna proposed no name as a term and nothing was locked (#275). - Names translated by the Book Bible are locked too when the analysis names a character in the target language and keeps its source form as an alias (« Canard », alias « Duck »): under
translate, that source form enters the glossary with the analysis’s translation and is locked if the book uses it often enough. A character still named in its source form gets no invented translation (#275). - EPUB export: the blank line between two paragraphs is kept when a long chapter was cut into two parts right there, so the last paragraph of one part and the first of the next stay two
<p>instead of one (#272). - EPUB export: a chapter whose text sits straight in
<body>(or a<div>), paragraphs separated only by blank lines, is exported with one<p>per paragraph instead of running its paragraphs and dialogue together (#271). - A book page no longer keeps a generic “Failed to fetch” banner once its data loads again: a failed refresh of the book (project, sections, jobs) is now shown on the book with a retry button and cleared by the next successful refresh; a book whose first load fails offers the same retry instead of loading forever (#270).
- An EPUB 3 navigation document (
nav.xhtml) or NCX listed in the manifest but not in the spine is no longer imported as a translatable section: only the spine documents are cut into passages, and the export still rewrites the table of contents from the translated headings. New imports use segmentation 3; books and archives cut before keep their sections (#269). - Compile each series name pattern once when passages build their contexts in parallel: a resumed analysis on a cold worker made ten threads compile the same names, so its first model request, and the other jobs of the worker, waited about twice as long (LIB-329).
- Autopilot no longer arbitrates a deferred passage again when its text, critiques, open points and model are unchanged and no stronger model is configured: the same question got the same refusal every round (about 1 h 30 per book). The passage keeps its reason in the final report; a changed input or a stronger model reopens the arbitration (#267).
- Open a large chapter in the editor in about a second instead of two and a half: each translation field was measured by a script (one forced layout per unit, quadratic on a 50-passage page); the mirror that paints the formatting codes now sizes the field through CSS alone (LIB-330).
- A resumed analysis sends its first request as soon as its first context is ready: a job now prepares its contexts one at a time, in order, instead of all together; other jobs are not held. A stalled OpenViking search is cut after 10 s and the context falls back on the database (#264).
Changed
- Compile name patterns built with
word_boundariesonce (boundedlru_cacheinengines/word_boundary.py) in quality checks, edition, memory search and propagation, instead of again on every passage (#265).
Security
- PyJWT 2.15.0 (GHSA-42vr-xj54-vc7v): the dependency audit refused 2.14.0 and held back
main.
Fixed
- A poem sent as JSON or TXT keeps its lines: a blank-line separated block of at least three short lines that start with a capital (not dialogue, not hard-wrapped prose, not a
Name: valuelist) becomes one unit with its line breaks, so it is detected as verse and goes to the higher model. Before, its lines were joined with spaces (Crocodile) or split into one paragraph each (Jabberwocky). Text exports and EPUBs give the lines back, Word exports as line breaks. Chapters imported before keep their cut, and their project archives restore as they were (layout.version2) (#312).
[0.25.0] - 2026-09-29
Upgrade notes
- One migration,
2b4e91c7d605, clears historicalcheckpassages with no remaining review reason. Passages with an open quality issue, critique, uncertainty or error stay in review. No licence protocol change; compatible with the deployed 0.12.3 licence server.
Tests
- Cover the full autopilot loop with multiple error suggestions, a rejected locked-term suggestion and a reported unresolved residual.
Changed
- Serialize the SQLite and PostgreSQL backend CI suites independently across merge request pipelines, while allowing
mainpipelines to run without waiting in those queues (#245).
Fixed
-
Repair historical
checkpassages with no critique, uncertainty, error or open quality issue during database migration; keep passages with a review reason and validated passages unchanged. -
Recheck accepted autopilot critique suggestions before clearing an error: a second reading that still finds the same error keeps the earlier translation and sends the suggestion back to arbitration. Completion reports now count applied, rejected and pending suggestions and explain each passage left for review after the bounded rounds.
-
Character, glossary and adopted wording changes now flag only affected passages, keep a readable quality issue for each pending recheck, and queue a bounded final review that clears the flag or leaves an explained review point. Decisions made during a review remain queued for a fresh pass. General style sheet changes only advance the memory revision.
-
A blocked autopilot report is recounted against the passages as they are now: once the listed passages are corrected or validated by hand, the report, the book status and the Export menu no longer say “blocked” and the translated EPUB is offered again (#262).
-
The autopilot final review no longer fails with “database unavailable” (
ArgumentError) on jobs whose options carrysegment_ids: null; it reviews the whole scope again (#261). -
The book page’s “Autopilot blocked” status line shows the convergence rounds, the reason and the required action on separate lines instead of one run-on sentence (#259).
-
Update EPUBCheck’s Jackson components to 2.18.10, fixing CVE-2026-68497 and the container vulnerability scan.
-
A book whose last autopilot run ended blocked no longer reads “Book ready to export” in the Export stage or the library: the progress now carries
autopilot_outcome(#260). -
Final review: a passage whose full context does not fit the provider window (the passage and its earlier critiques too large, or no room left for its neighbours) is now reviewed in the reduced context the arbitration already used: the passage, its mandatory rules, its current translation and the memory it names, then as many earlier critiques as fit. Eight passages of a 32,768-token window used to fail the same
ContextTooLargethree times and never conclude. When even the passage and its rules exceed the window, the review says so once and is not retried (#256). -
Autopilot: a book whose bounded rounds end with passages still in the source language, open review points or inconclusive checks now ends
blocked(“Blocked · not ready to export”) with the exact counts (source_passages,unresolved_passages,unverified_checks) instead of “Finished” with a download; a book without residuals stays ready. A scene break (* * *) is no longer flagged as text left in the source, and a revision written in the last round now gets its quality-judge check instead of reaching the report without any attempt. API requests still endcompleted_with_residuals(#255). -
Autopilot, blocked book: the status line and the report say what to do next (open the report and deal with the passages), and the Export menu labels the EPUB “Provisional EPUB · book blocked”; it can still be downloaded, it is no longer offered as the finished book (#255).
-
Prepare a passage of a long series about three times faster: its text is folded to half-width only when it holds a full-width character, instead of once per name the series knows (a volume after 18 others: 18 s → 6 s of CPU per passage) (#253).
-
Show every reason of a passage flagged for recheck after several memory changes (for example a character and a glossary term) in English when the interface is in English; the stored French text is unchanged (#250).
-
Quality: recheck points left by a memory or style change show a readable, translated title instead of the raw
memory_changed/style_changedcode (#250). -
Quality: a missing locked term shows a readable title instead of the raw
locked_termcode, and its reason (“Locked translation missing: …”) is in English in the English interface; check findings stored as<passage> : <reason>are now translated after the passage ID (#252). -
Quality and review: point and issue messages switch to the new interface language together with their titles, without reloading the page (#251).
-
Worker: running jobs no longer lose their lease and start again in a loop while the memory catalogue is rebuilt. The rebuild, a pass over every book that lasted minutes beside passages being prepared, now runs off the event loop, and a job taken back after its lease expired is logged (
status=reclaimed) (#253). -
Worker: a job whose run is still alive is no longer taken back when its process stalls for longer than a lease (a starved host, a frozen container). The poll of the same process reclaimed the job ahead of the heartbeat, threw away the provider call in flight and restarted the job (
status=reclaimed, attempts 8 → 9). A run alive in the process now renews and keeps its job for up to 10 minutes past its lease; a run that renews nothing for longer, or a job of a process that died, is taken over as before. A renewal that comes after more than 40 s is logged (heartbeat=late) (#254). -
Codex ChatGPT provider: the models check now returns the context window of each model from the Codex catalogue (
context_windows, 272 000 tokens for GPT-6 Luna and Sol) so the provider form no longer keeps the 32 768 default; models missing from the catalogue, or a catalogue that cannot be read, are left out and other providers are unchanged (#257). -
Codex ChatGPT provider: the form fills the context window with the catalogue’s for the detected or chosen model (272 000 for GPT-6 Luna and Sol) while the provider has no saved model yet; a window typed by hand or already saved is never overwritten (#257).
[0.24.0] - 2026-09-28
Upgrade notes
- One migration,
73c9e2a14d60, addsllm_requests.imported(false for existing requests). No licence protocol change; compatible with the deployed 0.12.3 licence server. This release groups the 0.23.0 audit corrections (#237), the backup and restore fixes (#243, #244) and the work prepared as 0.23.1, which was never tagged. - Translations and rewrites now pass an independent semantic-fidelity check: one more model call, on the job’s
provider, for every translated or rewritten passage. When that check cannot run or finds a divergence, the
candidate is refused, the earlier text stays and a human review point opens: expect more review points while
a provider is unavailable. Built-in prompts move to
file-v16+rules-v7; a prompt an administrator saved in Settings › Prompts keeps its own text. - Registry installations: rerun the installer (
LIBRIS_TAG=0.24.0) rather than pulling images, so that the backup and restore tools shipped in the image are installed next to Compose.
Changed
- The interface follows the brand charter shared with the website: titles and the wordmark are set in
EB Garamond (roman only) and the interface in Schibsted Grotesk, in place of Fraunces and Inter;
Source Serif 4 still sets the books, their italics included. The interface sets no italics of its own
(the sign-in rubric and series evidence labels are roman), titles lose the word spacing Fraunces
needed, and card titles move to 18 px. Rows of the import assistant and the series next actions show
their state on their whole hairline instead of a 3 px side stripe; quotes, evidence and AI proposals
use 1 px rules; a validated character is outlined instead of striped; empty states and the AI final
review show their icon without a tile. Under reduced motion, colour and focus feedback stay while
movement stops.
libris-logo.pngkeeps the owner’s artwork and sets « Libris » in EB Garamond, and the documentation screenshots are regenerated.
Added
-
A reproducible before/after report for the real translation engine on the annotated evaluation corpus, with model costs, confusion counts, and archived run manifests.
-
Exercise English dialogue, prompt rules and a human style choice through the full autopilot for French and Chinese sources.
-
A book’s canonical target title, and a chapter’s own edited heading, now reach translation and rewriting as locked-priority terminology (
backend/app/engines/context/canonical_title.py),context.builder._prepareand the reduced context of a passage recovery included. Only an editor’s explicit decision (the edition’smanual.titleormanual.chapters) attaches a target title to the source form it translates — never a guessed resemblance; two decisions translating the same source form differently inject nothing rather than a contradictory instruction. An existing locked glossary term or series decision for that source form still wins. Confirmed only within the passage itself, never a neighbour shown around it for continuity, and only as the title’s exact words — an ordinary word or phrase that happens to title a book or a chapter is common enough elsewhere that a neighbouring or an inflected occurrence (a plural, a declined form) would be a guess, not a confirmed reference. A person’s own decision on that source form, even left unlocked (a glossary import, a manual edit), wins the same way; a source form a character of the book already answers to (a same-case homonym included) is too unreliable a match to attach to the title and injects nothing either. Part 1 of libris/libris#242: harmonizing the title across OPF, navigation, headings and exports, and reporting a persisting divergence, is part 2. -
Semantic-fidelity verifier contract:
FidelityDivergence/FidelityVerdictschemas andprompts/fidelity_check.txtdefine an independent check — reading the source, the previous translation and the candidate rewrite, never the producing pass’s own self-assessment — for seven controlled assertion categories (negation, agent/action/patient, action result, quantity/scope, object identity/state, injury/death, named technique or culture-bound term). A 21-case synthetic multilingual corpus (backend/tests/fixtures/fidelity_corpus.yaml, FR/EN/ZH/JA) covers the five faults from GitLab libris/libris#241, faithful rewrites, intentional source contradictions and already-settled human decisions. Independent source-grounded checks now screen initial translations and candidate rewrites in revision, polish, fused review, final review, quality repair and arbitration; rejected candidates keep the earlier text and a review point. Completion reports distinguish clean checks, divergences and checks that did not run.
Fixed
-
Check dialogue quotation marks only on identified speech. Inline titles, technique names and cited words no longer raise dialogue findings; uncertain quotations remain low-severity review notes and cannot trigger an automatic final-review rewrite of names, quotations or speaker attribution. With dash-style dialogue, French and curly quotes around thoughts or letters remain untouched.
-
Keep a canonical title correction consistent in rebuilt and bilingual EPUBs and text exports. Confirmed machine passages adopt the new title; validated text and competing editorial or series decisions stay intact. Target-language common-word homonyms and book/chapter title collisions are left for review. Unresolved mismatches appear in the existing quality-issue list before export or publication, without reopening a warning the editor already resolved.
-
Show skipped glossary propagation counts and passage-level validation reasons in the book UI.
-
Find source glossary terms across half-width and full-width Latin spelling in context selection, quality checks and propagation, while keeping match spans aligned with the original text.
-
The style sheet now proposes and applies only values a target language’s own grammar and typography actually have: no tu/vous rule for an English target, no French guillemets rule for a target whose convention is a dash, the compound past kept only where it is a register of its own (French, Italian). A series decision inherited by a volume whose target does not share it is left out rather than silently applied — a couple’s tu/vous form included — while a volume’s own explicit decision, and any pair it sets itself, is never touched. A
profanityproposal,fidèleas much asatténué, now needs an actual instance of profanity in its quoted evidence, code side as well as in the prompt, or its confidence stays low. -
Book Bible consolidation no longer overflows the provider window when a single incoming synthesis merges into an existing bible: the character registry sent is limited to the identities the base bible or the fragment actually name, and an oversized single item is split by structure (its lists, in halves) instead of being dropped whole. When even the smallest fragment still cannot fit next to the existing bible, the size, budget and window are reported explicitly, no chapter is falsely marked consolidated, and the checkpoint keeps the size so a retry with the same provider and window does not repeat the identical oversized synthesis. A resume with a different provider or a wider window now retries the synthesis nodes an earlier attempt gave up on, instead of leaving them skipped forever (#238).
-
Refuse a translation or rewrite when its independent semantic check cannot run or returns ungrounded evidence; retain the prior text and open a human review point.
-
Ship backup and restore tools in the registry image and install them alongside Compose; preserve a saved
.envfor fresh restores and allow--no-startbefore restoring the database and books. -
Serialize automatic series corrections across volumes and remove legacy foreign-language volume terms while keeping manual terms.
-
Keep unchanged accepted arbitration decisions, localize their series correction count, and resume queued critique acceptances after the last processed item.
-
Permit rewrites of translations with already missing note calls, including accepted critique proposals; skip and report invalid glossary propagation passages, and preserve resolved quality alerts.
-
A volume moved to a different language pair — by an automation request, a resent EPUB, its own settings or the series’ language — no longer keeps the style values the autopilot guessed for the previous source→target pair: they are dropped and the style sheet is asked again for the new pair, while a person’s own style choices travel unchanged, even when the style sheet screen resubmits one of them with the same value the autopilot had guessed.
-
Restoring a project archive no longer deletes the usage history of a deleted book named by the archive. Restored requests are marked as imported history and never counted as new spending, including against the daily spending cap. A provenance signature tied to this installation, the original owner and the complete archive content permits reassignment of the deleted source book’s aggregates. Unsigned or previously signed archives remain importable without changing aggregates. Migration
73c9e2a14d60addsllm_requests.imported(false for existing requests). -
Prequels labelled
Vol. 0orVol. 0.5now precede volume 1 in reading order, series memory and EPUB position. The import wizard no longer proposes the invalid number 0 from a file name. -
Retry or safely fall back after invalid targeted translation repairs, and check locked terms in partial answers.
-
Complete automatically revised passages, let review and targeted jobs bypass whole-book consolidation, and clear stale Book Bible window failures on retry.
-
Keep optional narrative refresh failures from stopping translation, and report polish rejections caused by added warnings accurately.
-
Join a single two-line TXT paragraph wrapped near 70 columns without splitting it as a heading.
-
Keep DOCX notes attached to removed chapter headings in split API imports.
-
Parse valid YAML front matter with comments and quoted or spaced keys.
-
Report a missing or damaged translated EPUB cover as a clear conflict on export and preview.
-
Recognize common Korean name suffixes without matching longer names.
[0.23.0] - 2026-09-27
Changed
-
Autopilot arbitration now rechecks automatic findings and obtains a fresh semantic review of the merged passage before applying a correction. A rejected correction leaves its open points for another round; each rewritten revision still follows the review obligations.
-
Translation rewrites in polishing, fused review, final review, and the quality judge now share one finding-based acceptance rule: existing defects remain open, partial fixes can be kept, and new serious errors reject a rewrite even when its total issue count falls.
Upgrade notes
- Three migrations:
3b8d51f0c7a2removes the foreign key fromusage_daily.project_id;8e2f64c1d9b5counts the words of every existing passage (a few seconds for a large library);4cc7ed91a205adds the book edition choices (projects.edition, empty by default). No change to the licence protocol: works with the licence server already deployed (0.12.x).
Added
-
A short, hand-annotated evaluation corpus for translation quality (
backend/eval/corpus, seedocs/evaluations/corpus.md). Nine cases, three language pairs (English→French, Japanese→English, Chinese→English) and four formats (EPUB, DOCX, TXT, Markdown) with 46 annotations across fidelity, target-language quality, cross-chapter coherence and EPUB dimensions: ellipses, established attributes, late reveals, legitimate adaptations, intentional contradictions, source-only flaws and proven errors, each paired with a witness so a judge is checked against false positives as well as misses.backend/tests/test_eval_corpus.pyvalidates the schema and the aggregate bar (≥ 40 annotations, ≥ 4 per category, ≥ 1/3 witnesses). Running the engine against it and scoring the result is separate, later work (libris/libris#199). -
Book edition choices. The book page now edits the published title, language, chapter headings, cover choice, cover alternative text and optional translated cover image through the shared edition API. Saved artwork is previewed through its protected URL, and edition validation errors appear in the editor.
-
Real-pipeline evaluation harness. A cost-capped command imports annotated corpus cases, runs the translation pipeline and EPUBCheck, preserves model contexts and evidence, reports confusion counts and delivery metrics, and compares two runs in Markdown.
-
Word counts for every book and every series. The library’s cards and list, the book page and the series page (volume by volume, and in total) show the words of the source, counted exactly as the licence counts them (a run of Japanese or Chinese characters is one word per two characters). The count is kept with each passage and follows a chapter replaced; the series total leaves out archived volumes, like its other totals. The Settings › Book card shows the same live figure instead of the one noted at import.
Fixed
-
The narrative events of a translated passage are kept again when the target language’s typography changes its text (a French apostrophe, a no-break space before
?): they were discarded, so later passages lost what had just happened in the story. -
A recovered passage whose final persistence validation fails now tries the next recovery rung; the job continues with other passages and reports any remaining failures (#201).
-
Book Bible synthesis now splits merges to fit the provider’s context window. Autopilot blocks translation when sections remain unconsolidated and exposes their IDs through the job API. The book page lists affected sections, offers a consolidation retry, and asks for explicit confirmation before resuming translation with
allow_unconsolidated=truedespite the consistency risk. -
The Korean locked-term regression test now uses an unambiguous proper-name occurrence, so it checks that a missing required translation blocks without conflicting with sentence-initial homographs.
-
TXT import and chapter splitting now decide hard wrapping for each blank-line separated block, preserving short status windows and verse as separate lines alongside genuinely wrapped prose, including prose split into repeated two-line blocks (#218).
-
A provider refusal during the book-wide consistency check now blocks the job at book level without marking the last successfully translated passage as refused.
-
Locked German glossary phrases now accept weak adjective endings after an article, such as “Schwarzer Ritter” in “der Schwarze Ritter” or “des Schwarzen Ritters”.
-
Korean locked terms now remain composed after accent folding, so attached particles and endings are accepted in translation checks and series consistency audits.
-
Series glossaries now collect terms only from volumes matching the series language pair, so a locked term from another translation language cannot replace the series term or trigger a false consistency finding (#227).
-
Automatic quality checks now reject a paragraph copied into another when the source has no such repetition. Polishing and quality-judge rework reject a moved phrase instead of restoring it twice (#231).
-
Korean locked names are now recognized only as complete names or with Korean particles and copulas, including chained particles. A name at the start of a longer word no longer wrongly blocks a correct translation (#211).
-
Latin glossary terms and character names adjacent to Japanese, Chinese or Korean text now reach the translation context, source and target checks, autopilot counts, and term propagation.
-
Narrative events now follow the saved translation revision. Meaningful rewrites invalidate stale events in chapter context and SQL/OpenViking retrieval; memory is rebuilt when next needed. Typographic edits preserve it, and source analysis remains intact.
-
Translation and revision now retry missing or empty paragraphs together while keeping valid paragraphs, matched by unique unit ID and restored to source order. Added or reordered units are handled when matching is unambiguous; duplicate IDs and exhausted targeted retries use the existing full repair path.
-
AI proposal acceptances recorded as their queue job finishes now keep that job pending until the new acceptance is processed, instead of leaving the proposal queued indefinitely.
-
The quality judge’s correction cap now uses only the passages a job translates. A follow-up job cannot spend its 25% allowance on passages that were already finished; the base stays fixed if the job resumes.
-
Markdown imports now remove a hard line break’s trailing backslash when the next line belongs to the same paragraph, while preserving escaped characters and literal backslashes.
-
EPUB reading exports (TXT, Markdown, bilingual EPUB and chapter ranges) retain visible code, preformatted text and other content intentionally left untranslated in its original position.
-
HTML and DOCX imports now omit ruby readings instead of joining them to the base text. DOCX also skips the old position of tracked moves and imports moved text only at its new position.
-
Accepted editorial proposals now refresh automatic quality alerts on the saved paragraph before its status is set. Source-language text remains flagged, and corrected untranslated fragments close their alert.
-
EPUB chapter headings split by
<br/>now keep a space in chapter names, text exports, and the translated EPUB’s navigation and document title. Text exports show the translated heading once. -
Capitalised locked terms in uppercase passages now use the same source-language matching rule in the prompt and quality check, including overlap with shorter locked terms.
-
DOCX import now keeps footnotes and endnotes: numbered calls remain in the chapter text, and their translatable text is placed at the chapter end with an import warning. Word export writes these notes as ordinary paragraphs.
-
A human correction or decision to keep the source made during an in-flight model call now keeps its status and alerts when the pipeline finishes that passage. Recovery counts exclude passages that recovery itself cannot select.
-
Markdown chapters beginning with a horizontal rule now keep their first scene; front matter is removed only when it contains metadata keys, including keys with YAML lists.
-
Quality control now checks that a DOCX-imported note call (
[3]) survives in the translation. It is plain text, unlike the immutable EPUB markers checked separately: without this, a call dropped from the body or from a note went unnoticed. Checked as a multiset per unit (body or note) and only reports a source call going missing; a call the translation adds without a source match is not this check’s concern. -
Rejoined translated parts of long paragraphs with the target language’s sentence spacing in rebuilt EPUBs and text exports. Thai and Lao phrases also keep their spaces; Japanese and Chinese remain unspaced.
-
French typesetting keeps a single inline code or code pair quoted alone exactly once, so saving a valid machine translation and delivering older translations no longer duplicate its EPUB markers (#217).
-
The quality judge’s accepted correction now keeps review points on paragraphs it did not rewrite. Remaining points leave the passage To check; points on rewritten paragraphs are removed.
-
Kept translation parts are reused after a retry only when their source text has the same cut, preventing repeated or missing sentences when a smaller context window changes the split.
-
Multilingual TBX imports skip entries missing a language used elsewhere in the file for the book’s source or target, instead of importing a term from another labelled language. Positional fallback remains available for unlabelled sections and files without the book’s language.
-
Word exports now use the reading text for EPUB and TXT chapters: inline codes and metadata stay out of the document, each book paragraph gets its own Word paragraph, and the translated chapter heading appears once. Tracked revisions and doubt comments remain attached across paragraphs.
-
EPUB marker errors retry only the affected paragraphs during translation, revision, and autopilot arbitration. Valid paragraphs are kept. After two failed targeted retries, formatting markers are restored by alignment when image and note markers are intact; otherwise the existing fallback applies. Retry counts, token usage, and restorations are logged.
-
Series characters sharing only a title or alias now stay separate. Such a match is proposed for review without merging aliases; automatic links require a canonical-name match and compatible genders.
-
TXT import keeps Chinese and Japanese paragraphs separated by single line breaks when they end in CJK punctuation, and joins genuinely hard-wrapped CJK text without adding ASCII spaces.
-
Glossary propagation leaves passages deliberately kept in the source language unchanged. Its preview and report exclude them, preserving their retention alert and incomplete coverage.
-
A locked proper name that also spells a common word (such as “Will” or “Dawn”) now raises only a warning when it appears solely at sentence starts, where capitalization is ambiguous. The prompt omits its mandatory glossary instruction in that case but still gives the decided translation as ordinary glossary guidance; occurrences within a sentence remain enforced. A term that could never be mistaken for an ordinary word (numbers, punctuation) keeps its full enforcement even at a sentence start.
-
Locked French glossary terms now accept agreement in each part of a hyphenated compound (such as
loups-garousandGrande-Prêtresse) and the irregular title pluralsMesdemoiselles,Messieurs,Mesdames,MesseigneursandGentilshommes. -
Referent evidence is now budgeted per action, with its source quotation and slot citations kept together. Omitted actions are recorded in the context inspector and cannot reach the translator as facts. In a twelve-referent audit, the default 32k window kept all twelve; a 1,000-token optional cap kept one and recorded eleven omissions. Scoped reader decisions remain mandatory even when optional character sheets are dropped.
-
An unsupported character claim could no longer set gender, pronouns or role, but survived in free text. Audit A3 (2026-09-27): the grounding filter (
grounded_analysis) already blanked a structured attribute without a source citation, but the very same affirmation stayed usable throughdescription,relationshipsortranslation_notes, so it still reached book memory and the translator’s prompt as if it were settled — an invented gender or profession came back as “a female physician” or “Use she/her”. These three free-text fields now carry the same citation contract as gender, pronouns and role:description_evidenceand, one per item,relationships_evidenceandtranslation_notes_evidence, each checked for an exact match in the passage before the field is kept, and the citation must also be anchored to the character it claims to describe — it must name them, or share an unbroken paragraph with a passage that does — so an unrelated sentence elsewhere in the passage cannot pass as evidence for them. A sentence that genuinely names and establishes two characters at once (a shared parentage, a shared profession) still grounds both: sharing a citation is not, by itself, a reason to reject it. The parallel analysis mode grounds each passage’s extraction before it joins the reconciliation timeline (app.engines.translation.parallel_analysis), so an unsupported claim cannot become “known” for a neighbouring passage before it is filtered. A validated human decision is untouched: it is layered onto the sheet after this filter runs, never through it. The built-in prompts move tofile-v13(chapter_analysis,chapter_extraction,chapter_reconciliationnow ask for the same per-claim citations on free-text fields). -
Arbitration no longer retries a critique whose suggestion is already in its paragraph. It skips already applied suggestions despite spacing or quote typography differences, and accepts an unchanged paragraph when its accepted suggestion is already present.
-
Autopilot checks the text written by arbitration before settling a book. Each actual rewrite receives a final review of its new revision and a targeted consistency check with neighbouring passages and relevant relations, including on later rounds. Unchanged passages reuse their verdicts; retries remain bounded and concurrent human edits remain protected.
-
Autopilot review obligations now survive a failed model answer, later rounds and worker restarts. Each required final review and configured quality judge check is tracked for its passage revision. Missing checks are retried within the round limit and remain explicit in the final report when exhausted.
-
Review findings with an unknown passage unit now trigger bounded response repair in the quality judge, separate review, final review, fused review, and consistency check. Invalid findings remain in request diagnostics; exhausted attempts report an invalid review instead of accepting an empty verdict.
-
A foreign-script term quoted and explained is no longer treated as a fragment left untranslated.
foreign_script(the single check every quality pass, the arbitration and the final review share) now reads structural cues before raisinguntranslated: the quotation holds a single cited word or short name — no space or sentence punctuation it could break a clause on, and short where the script has no space to bound a word by — the same unit is quoted in the source, and some comment surrounds it in the target. A translation that kept 雨宿り, дождь or νερό to explain the word no longer failed its checks, had that failure reopened at every arbitration round, and had the correct candidate refused by the final review for fixing it. A genuine omission is still reported, quoted or not — including a line of dialogue whose quoted speech stayed untranslated behind a long, well-translated narration (an early version of this rule excused it merely because the surrounding comment ran long; the length of the quoted unit itself, not of what surrounds it, is what now decides). -
Translation memory now respects passage order and instructions. It reuses only earlier passages in the same book or earlier series volumes, with matching local instructions (and matching book instructions across books). Short passages are translated in their current context. Human corrections follow the same limits.
-
The series consistency audit now runs before the autopilot’s final report, and resolves what it can on its own. A term or a character’s gender left to diverge by the models across a series’ volumes — never a deliberate variation, a locked term, or a gender choice a person already validated — converges automatically, bounded, toward whichever of those already carries a human decision, or else (for a term, absent any decision) toward what the earliest volume established. A decision is never propagated to a volume that precedes the one it was made in, so a later revelation (a name kept secret, a gender disclosed further into the series) is never overwritten backward; an undecided gender change appearing in a later volume is left alone for the same reason. Style sheet conflicts are unaffected and stay manual. Only a genuine conflict between two such decisions is left open, and its count now contributes to the job’s report so a residual outcome reflects it instead of a bilan computed before the fix.
-
Deleting a book no longer removes its tokens from the statistics. The daily usage aggregates were deleted with their book, and the requests not rolled up yet went with it: Statistics and the Prometheus counters lost everything the book had spent. A deleted book’s aggregates now stay, and its latest requests are counted in them before it goes. Restoring the archive of a deleted book (archives exported from this version on) takes its place in the statistics instead of counting it twice.
-
EPUB editions keep editorial choices consistent across previews, exports and delivery. Chapter titles follow the import navigation anchor or the first heading at any level; unchanged rich headings keep their markup and manually changed headings keep their anchors. Cover artwork is stored as a project file, carried by project archives and served through a dedicated endpoint. Its signature must match the source EPUB manifest type.
-
Deleting several volumes of a series at once no longer fails. The library’s batch deletion sends one request per volume, in parallel. Each deletion committed, then refreshed the series memory in a second transaction: one deletion’s cascade (the series terms’ and identities’ first volume, the series links) crossed another’s refresh, and PostgreSQL killed one of them — a deadlock, or a refresh writing a volume just deleted. The volume was gone, the request answered 500 and the series memory stayed as it was. A volume of a series is now deleted under the series lock, deletion and refresh in one transaction: the deletions of a series queue and every one answers.
[0.22.0] - 2026-09-26
Upgrade notes
- No migration, no change to the licence protocol: works with the licence server already deployed (0.12.0).
- The built-in prompts move to
file-v11(by way offile-v10, never published): the prompts that translate, review, revise, arbitrate, polish, judge or analyse a passage name the two new series sections (see Added), andtranslationandbook_analysissay the memory stops at the passage (see Fixed). A version of one of them an administrator saved in Settings › Prompts keeps its own text: compare it with the built-in one. - From its second volume on, a series volume’s prompts carry the series memory: at most 2,400 tokens by Libris’
conservative estimate (one per byte, about a quarter of it in real tokens) for
SERIES_MEMORYand 600 per character of the passage an earlier volume knew, taken within the optional context budget (Context budget, 32,000 by default since this version, see below); measured on a heavy volume 1, about 850 real tokens more per call. - The character sheets are dated by the analyses a book already holds: a book analysed before this version is dated as it stands, nothing to run again. A fact of a sheet that no stored analysis states (a sheet older than its analyses, analyses deleted with a replaced chapter) counts from the character’s first appearance, as before. A passage analysed again by a catch-up or a forced analysis before this version read the whole book and may hold a link the story makes later: Full reanalysis dates it again.
- A translation prompt no longer carries the Book Bible’s summary (up to 350 words); it carries instead the summaries of the three chapters before the passage’s (600 characters each at most) and of its own chapter up to it (900 at most). On the audit’s synthetic book the translation prompts are 12 % smaller on average.
- Larger prompts on large windows. The optional context of every call (sheets, glossary, Book Bible, chapter
state, neighbours, memory) now takes three quarters of what the provider’s window leaves once the mandatory parts
are placed, up to Settings › Memory · OpenViking › Context budget (
context_budget), whose default moves from 12 000 to 32 000 estimated tokens. A stored 12 000 — the former default, which the settings form saved with every OpenViking configuration — is read as 32 000; any other value is kept. Measured on a realistic passage (3 400 characters, 8 characters, 30 terms, 10 relations, 4 096 output tokens), a translation request grows from about 27 600 to about 47 700 estimated tokens with a 64k window (about 6 500 → 11 500 real tokens, some 5 000 more per call; the Book Bible, first in the prompt, can be served from the provider’s prompt cache), and barely changes with a 32k window (about 27 600 → 27 800: its optional context stays at 12 000). A volume of a series whose earlier volume was fully analysed (64k window, 16 384 output tokens) goes from about 27 800 to about 36 200: its series memory now keeps all six characters instead of four. To keep the former size, save a Context budget of 11 999 or less.
Added
- A volume of a series is translated with what its earlier volumes established. The Series Bible, the series
relations and what a person decided in volume 1 reached no prompt of volume 2: a volume inherited the terms, the
corrections and the names of its earlier volumes, nothing else, so volume 2 did not know what volume 1 was about,
how two characters stood at its end, the gender a reader had decided nor the tu or vous its style sheet set. Every
prompt of a series volume (translation, reviews, revision, final review, arbitration, polishing, quality judge,
analysis) now carries two sections built from its earlier volumes only (same series, owner and language pair,
earlier in the reading order), never from a later one.
SERIES_MEMORY, the same for the whole volume: the Series Bible a person validated, else the conventions of the earlier volumes’ bibles, and “previously in the series”, the earlier volumes’ summaries, the most recent first, with how the last one ended.SERIES_CHARACTERS, one per character of the passage an earlier volume knew, as the most recent one left them: names, role, gender and pronouns (decided_by_readerwhen a person decided them), register and voice, the latest relation with each other character of the passage and the forms of address a volume’s style sheet decided. A validated Series Bible reaches a volume without what it dates from that volume on (chronology entries, characters by their first volume) nor what a later volume wrote word for word; its undated content (universe, conventions, a note) is the person’s rules for the series and is sent.SERIES_MEMORYcomes right after the neighbouring passages, before any other memory, and never cuts into their share; a character follows this book’s own sheets, and this book’s rules, sheets and decisions come first where they differ. The reduced context of a passage too long for its window keeps the gender, pronouns and register the earlier volumes decided for the characters it names.
Fixed
- The series audit compares only volumes of one language pair. A series translated into French and into Spanish reported every term as translated two ways and every character as having two genders, and Apply to the whole series wrote the French translation into the Spanish volumes’ glossaries. The audit now compares the volumes of each language pair (primary subtags) among themselves, holds only the volumes of the series’ own pair to its locked terms, and an answer applies to the volumes of the pairs where that question was asked.
- Series relations follow their volumes. A relation between two series characters was written once, with the wording of the first volume that stated it, and never updated nor removed: deleting or detaching a volume left its relations in the series (the Relations tab, the Series Bible), and a volume stating a relation again, otherwise, changed nothing. Every refresh of the series now recomputes them from the volumes: a relation starts where the reading order first states it, reads as its latest statement, and goes when no volume states it any more.
- A volume never receives a character’s name from a later volume. A character’s series identity was named after the first volume analysed: volume 3 analysed before volume 1 made “Lelouch vi Britannia”, the name volume 3 reveals, the name volume 2 was told to keep for the masked man Zero, and the identity started at volume 3. A volume now receives each character under the names its earlier volumes used, the most recent one’s first; an identity starts at the first volume of the reading order linked to it and, unless a person validated it, takes that volume’s name and sheet (the later name stays an alias), so the series memory is the same whatever order the volumes were analysed in. Places, organizations and objects are dated the same way.
- A passage’s prompt no longer knows what the story reveals after it (audit of 2026-09-26, H2). The character sheets were the whole book’s: a sheet takes its longest name and gathers the names, the gender and the relations of every chapter, so “Graymask = Mira Voss”, revealed at passage 13 of the audit’s book, was in the prompt of the 13 passages before it; the free-text relations of a sheet (“brother of Nunnally”) escaped the dating of the relation graph. Every name and fact of a sheet is now dated by the analyses stored passage by passage, and a prompt only receives what the story established by then: a masked figure keeps its own sheet under its own name until the story unmasks it, a relation written on a sheet appears from the passage that states it, and relation endpoints, the reduced context and the analysis’ own character register follow the same rule (an analysis reads what the passages strictly before it established). A gender the story only settles later is left out, and the sheet tells the model, without saying it, to keep the source’s ambiguity. What a person decided — a gender or pronouns, the names of a confirmed identity, a validated sheet — applies at every passage; when it runs ahead of the story, the sheet says so and the model is told not to disclose it. The Book Bible and Characters tabs still show everything.
- The Book Bible no longer tells every passage the ending; the story so far does. Its summary, written from the whole book, went with every prompt from the first passage, and the chapter summary never reached a translation (it was only sent once its chapter was over, which it never is for a passage of that chapter). A passage now receives the Bible’s fields that do not tell the story (genre, tone, narration, tense, audience, guidelines, wordplay, honorifics, conventions: still the prompt’s cached prefix), the summaries of the chapters before its own, and its own chapter’s summary up to the passage before it. A Bible a person validated is sent the same way: what it decides keeps their authority, its summary stays in the tab. The Book Bible prompt now keeps plot, hidden identities and twists out of every field but the summary.
- A catch-up analysis no longer reads the rest of the book, nor takes its chapter back. A passage the first round lost is asked again once every other passage is stored (#161): it read the sheets of the whole book and the summary of its chapter’s end, and recorded them at its own passage (on the audit’s book, one stored spoiler and 8 prompts); it then replaced the chapter summary with its own, marked through itself. It now reads what precedes it only, and a chapter summary only moves forward; what a person added to a chapter stays.
- A forced analysis starts over (audit M9). Full reanalysis deleted the passages’ analyses but kept the sheets and the Book Bible: the new analysis of passage 1 read the old sheets and wrote their links back there (80 analysis prompts of the audit’s book), and a Bible the autopilot had validated was never rebuilt. What a sheet holds from the old analysis is now kept out of the prompts until the new analysis states it again, a Bible the autopilot validated is reopened and rebuilt, and a Bible a person validated stays theirs.
- The test harness of the analysis (
tests/analysis_world.py) read only the last of the sections a prompt repeats: it saw one leaking prompt where there were 13. - A passage gets the memory of the names it declines. The glossary, the character sheets, their relations and the
series terms of a passage were chosen by the exact name: a Russian passage saying Наташу for Наташа, a German one
saying Peters for Peter or an English one saying Demon Lords received none of them, and with a Polish, Czech,
Finnish, Turkish or Hungarian source most names are declined. The context now looks for a name as the source
language inflects it: the whole name and the short endings of its cases (Russian, Ukrainian, Belarusian, Bulgarian,
Serbian, Polish, Czech, Slovak, Slovene, Croatian, Lithuanian, Latvian, Greek), of its suffixes (Finnish with its
consonant gradation, Estonian, Hungarian, Turkish after the apostrophe of a name and with its softened consonants),
German cases and adjectives, English plurals and possessives, Dutch and Scandinavian endings, Romance plurals, the
prefixes of Arabic and Hebrew; never a truncated start (Мечник is not Мечта, Jana is not Jan). Conversely a
capitalised name keeps its case, as the output check already did: “Will” no longer brings Will’s sheet to “I will
go”. Scripts without spaces are unchanged. The same rule chooses the characters an earlier volume of the series knew
(
SERIES_CHARACTERS), the reduced context and the characters of the consistency check. - A passage told in pronouns gets the sheets of its scene. “She laughed softly.” named nobody, so no character
sheet reached the prompt, although that is where a gender decides the translation — and a Japanese, Chinese or
Korean sentence often leaves its subject out altogether. The context now adds the sheets of the characters of the
scene: those the analysis of the passage lists, those the events of the passages just before say know something,
then those named last in the twelve earlier passages of the chapter, most recent first; at most four, only names
that point to one character, with their relations between characters of the scene only; each sheet tells, like the
others, only what the story established by then. They give way first when the window is short, and the context
inspector shows why each was chosen (
why). Up to four sheets more for such a passage (a few hundred tokens each), within the same budget. - The internal memory finds the memories of a Japanese, Chinese or Korean passage. A memory was scored by the
words of three letters or more it shared with the passage; in a script without spaces a “word” was a whole clause,
so a memory naming the same character as the passage scored 0 and was never offered (a two-character name was not
even a word). In English the reverse happened: an unrelated memory scored 0.5 on “the”, “was” and “that”, and a
memory’s field names (
summary,event) matched any passage using those words. Memories are now scored by the words that carry meaning (the frequent function words of English, French, German, Spanish, Italian, Portuguese, Dutch, Russian, Ukrainian, Polish and Czech no longer count), by the pairs of adjacent characters in scripts without spaces (less pairs of particles or grammar characters), and on their text only. Applies to theinternalbackend and to the database part ofhybrid. - The context uses the window it has, and gives way in a sensible order. The optional context was capped at 12 000
estimated tokens whatever the window — about three thousand real tokens of memory for a 64k model — by a setting
stored with OpenViking’s although it applies to every backend, and it was sacrificed by authority alone: an
unvalidated Book Bible went before any sheet, relation or chapter state (the final review and the arbitration lost
it first), the book’s unlocked terms were cut without a word (96 of 400 kept), and the unlocked series terms, all
mandatory, overflowed the window (400 unlocked series decisions made the passage untranslatable). The optional
context now takes three quarters of the room the window leaves, up to Context budget (see Upgrade notes), and is
filled in this order: validated human decisions of the memory, the nearest passage on each side, a person’s unlocked
series decisions, the Book Bible (validated or not) and the story so far, the sheets of the characters named, the
unlocked terms of the book and of the series, the state of the chapter, the sheets of the characters of the scene,
the relations, the farther neighbours, and last the retrieved memory; an early kind that would take more than its
share (a quarter, half for the Bible) waits for a later rank. Unlocked series terms are optional and bounded (the
kept ones stay in
SERIES_CONVENTIONS); locked ones stay mandatory. Everything left out — the farther neighbours by passage, each unlocked series term — is listed in the context inspector with its reason, and the newbudgetentry sums it up. The byte count that keeps the window safe is unchanged. - A locked term reaches the model with its description. The locked glossary gave the model the source and the translation only: the description a person wrote (“a title, never a name”, “the elves’ word for the spring”) was sent with the unlocked terms but never with the locked ones, so a mandatory translation was applied blind. Each locked term now carries the first 160 characters of its description, when it has one (at most about 170 estimated, 40 real tokens per locked term the passage or its neighbours say; the reduced context keeps its terms without description).
- An OpenViking key that became unreadable no longer empties the memory. When the
SECRET_KEYchanged, the stored OpenViking key could no longer be read: search was switched off, and a volume on theopenvikingbackend then kept only the validated decisions of the database — no memory at all, with an empty error in the context inspector. The database’s memory now stands in, whole, as when OpenViking fails, and the inspector’s retrieval error says the key is unreadable and must be saved again. A search switched off in the settings falls back the same way (openviking_search: disabled).
[0.21.0] - 2026-09-26
Upgrade notes
- No migration, no change to the licence protocol: works with the licence server already deployed (0.12.0). Books already imported are left as they are: the lines of their source site stay in their passages.
- New optional settings:
AUTOPILOT_STYLE_MIN_CONFIDENCE(0.8) andQUALITY_JUDGE_PROVIDER(empty: no quality judge), both also in Settings › Autopilot, andQUALITY_REWORK_SHARE(0.25). A book may name its own judge, or none, in its settings. - Each volume’s analysis now ends with one more model call,
style_proposal, when its style sheet leaves a field open (a few thousand tokens, once per volume). With a quality judge, each passage costs one more call on the judge’s provider, and a correction and a second reading for the passages it finds errors in. - The built-in prompts move from
file-v5+rules-v3tofile-v9+rules-v6: two new prompts,style_proposalandquality_judge;consistency_check,review_revision,autopilot_arbitration,polishing,final_reviewandtranslation_revisionchange (see Fixed), and the lists of mistakes cover more languages (see Added). A version of one of them an administrator saved in Settings › Prompts keeps its own text: compare it with the built-in one.
Added
- The mistakes of more language pairs are named in the prompts. Until now only a translation into French was told the mistakes a model makes without seeing them (#180); every other pair got nothing. English now has its own list, whatever the source (articles and number from a language without them, one narrative tense, the pronouns a source leaves out, dialogue that sounds spoken), and so do Spanish, German, Italian, Portuguese, Japanese, Chinese and Korean (the grammar and the translationese of each). The web-novel pairs Japanese, Chinese and Korean → English are told their own (honorifics and address terms, particles and onomatopoeia, katakana words, set phrases, cultivation terms, speech levels), and English → Spanish, German, Italian, Portuguese, Japanese, Chinese and Korean their false friends, calques and idioms. As for French, the writer avoids them, the polisher corrects them, a reviewer looks for each and the arbiter weighs corrections by them; the style sheet and the user rules outrank them. They add 100 to 350 tokens to each call of these pairs, served from the provider’s prompt cache.
- The lines a source site writes into every chapter are proposed for exclusion at import (#182). A site that
serves chapters to copy adds its own lines at their start or end (“Visit and read more novel to help us update
chapter quickly.”, “Use arrow keys (or A / D) to PREV/NEXT chapter”): imported with the text, they were counted,
translated, reviewed and arbitrated chapter after chapter (24 of 178 arbitration decisions of one web novel were
about their translation). For TXT, Markdown, HTML and DOCX chapters, including the chapters of a split file, the
import assistant now lists the lines found among the first and last five paragraphs of at least three chapters and
30 % of them (the same line whatever its spaces and case), with Exclude these lines ticked. What stays ticked is
not imported, so neither counted in the licence’s quota nor translated; the import estimate counts without it. The
choice is recorded on the book (
import_meta.excluded_lines) and applied to every chapter added to it later — by the assistant, the automation API or a followed source, which have no preview and only apply what the book recorded — at the edges of a chapter only: the same words inside the story are kept, and a chapter made of nothing else is kept whole. The stored source files and project archives keep the lines; a translation never shows them. - A word mixing the letters of several scripts is reported (
mixed_script, a warning on the translation, with the word and how many there are). Sites stamp their name into chapters in a disguised form that changes from one chapter to the next (freeweɓnovel.cѳm,ƒгeewebnovёl.com,fɾeewebnoveℓ.co๓): 39 passages of the same book carried it into their translation unnoticed. A word is reported when it mixes confusable alphabets (Latin, Cyrillic, Greek, Armenian…), or when a Latin word carries an IPA letter, a letter-like symbol or a digit of another script; accented Latin (French, Polish, Czech, Romanian, Vietnamese) and Japanese mixing kanji, kana and Latin are never reported. - The style sheet is proposed from the book (#183). A sheet left empty left the register, the tense or the
profanity to the model passage by passage, and the translation of a first-person, present-tense, colloquial and crude
narrator drifted from « J’abhorre » to « partenaires de dating ». At the end of the analysis, one light call reads the
opening of the book, a few passages further on and the Book Bible, and proposes a value for each field still open, with
a confidence and a quotation of the source; honorifics only when the source uses them, a value the sheet does not
allow is dropped. Under the autopilot, a proposal reaching
AUTOPILOT_STYLE_MIN_CONFIDENCEis written into the volume’s sheet and every proposal is logged (analysis/style_sheet); otherwise the proposals wait in the style sheet screen, each with Apply the suggestion, and Suggest from the book asks again at any time (POST /api/projects/{id}/style/propose,402without a licence). A field a person or the series decided is never written over, and a proposal that fails never holds the translation back. The prompt,style_proposal, is edited in Settings › Prompts like the others. - A quality judge reads every translated passage and has its errors corrected (docs/quality.md). A model of
another provider — Codex through a ChatGPT account, or any OpenAI- or Anthropic-compatible API — reads each
passage just before it is finished, with its context and the mistakes of its language pair, as the last editor
before publication (operation
quality_judge). Its errors (a change of meaning, a dropped pronoun or object, an omission, a grammar or spelling mistake, a calque or false friend a native reader notices…) are corrected on the judge’s model, through the polish’s guards, and the correction is kept only if the judge then finds fewer errors, withinQUALITY_REWORK_SHARE(25 %) of a job’s passages; the errors it still finds join the review’s points on the passage (its warnings only feed its correction). The judge reads the job’s instruction like every review, and is never swapped for the stronger model. It is chosen in Settings › Autopilot (QUALITY_JUDGE_PROVIDER) among the installation’s own providers — a member’s provider never reads other people’s books — and a book may name another one its owner may use, or none; the choice is checked again at each passage. Without a judge, nothing changes, and a judge that does not answer never fails a passage. Nothing runs locally: a small VPS is enough. A translation metric, MetricX-24, was measured first and set aside: on twenty corrected paragraphs of an English → French light novel it found the gross errors but not the calques (8 in 20), and it needed PyTorch and several gigabytes of memory.python -m app.benchhas the judge compare systems on a file or two volumes (errors, warnings, and chrF against references) before a release.
Fixed
-
An audit of this release’s changes tightened what was merged in a hurry:
- the import assistant lists each line of the source site with its own checkbox, so that a recurring line of
the story (a system message, a catchphrase) can be kept; the estimate counts the lines left unticked
(
keep_line); - a copy of a book into another language no longer carries the style sheet values the autopilot decided for the first language (French quotation marks for an English book): they are proposed again; asking for proposals again keeps the record of what the autopilot applied;
- a style proposal whose quotation is not found in the book stays below the autopilot’s threshold, and a narration in the compound past is proposed only for French and Italian;
- the bilingual export counts a passage left in the source language as missing, like the text export; the Word export takes out the quotation marks a model added to an unquoted thought, like the other outputs; corner, full-width and CJK quotation marks (「」 〝〟 " 〈〉 《》 ❝❞) now count as the source quoting;
- a term whose model description was longer than the glossary allows can be edited again (MCP
edit_term); - the lists of mistakes no longer forbid what is correct: a pluperfect a speaker needs, avoir with a verb of movement that has an object (« il avait sorti son épée »), the possessive on a part of the body that is the subject (« ses yeux brillaient »), a narration in the present in an English translation; several false friends are named with their sense.
- the import assistant lists each line of the source site with its own checkbox, so that a recurring line of
the story (a system message, a catchphrase) can be kept; the estimate counts the lines left unticked
(
-
A consistency remark is closed only by correcting the paragraph it is about. The book-wide consistency check stored its remarks without the paragraph they named, so the arbitration read each one as a remark on the whole passage: it “accepted” a remark on Alice’s forms of address without changing a word, or rewrote another paragraph, and closed the remark either way. A remark now names its paragraph first, as the automatic checks do: accepted, it must correct that paragraph and no other, otherwise the answer is refused and the remark stays open for the next round. Remarks stored before this version are still read as about the whole passage.
-
The consistency check judges forms of address with the rules and relations that decide them . Its samples carried the book’s instructions and the raw sheet of their subject only: no style sheet (so not the pairs that say who says tu to whom), no relation between the characters, not the job’s instruction, and a sheet whose decided fields were named
decidedwhile the prompt looks fordecided_by_reader, so a gender decided by a person read as one more guess. Each sample now also carries the user rules (the book’s, the job’s instruction, the style sheet’s rules, and a passage’s own and its chapter’s), the relations between the characters of its passages set before the last of them, with their position (the validated and latest first, at most 12), and the sheets of the other characters there whose gender or pronouns a person decided (at most 8); the subject’s sheet names those fieldsdecided_by_reader. A few hundred tokens per sample. -
The reduced context keeps the memory the passage names. The context a passage falls back on when its full context does not fit the window (the arbitration) or keeps failing (rung 4 of the recovery ladder) held the locked glossary and the style sheet only: not the confirmed identities and series conventions a full context declares mandatory, no character sheet (a gender a reader decided could be undone there), no unlocked term, not the job’s instruction. It now also carries the job’s instruction, the confirmed identities and series conventions of what the passage itself names, a minimal sheet of each character it mentions (aliases, gender, pronouns, register, what a reader decided) and the unlocked glossary. It stays reduced: no neighbours, book context, relations or retrieved memory, at most 8 sheets and 30 terms of each kind, and on a short window the memory gives way before the rules. On the audit’s passage its user message grows from 800 to 1,480 tokens, half of a full context’s.
-
A job’s instruction reaches every step that judges or rewrites its passages. The instruction given to one job (“Alice says vous to the king”: Retranslate › With an instruction…, or
instructionin the automation API and the MCP tools) reached the translation, the polish and the revisions only. The review, the review and revision in one call, the final review, the arbitration and the repair in groups of paragraphs judged or rewrote the passage without it, and the fused review could undo what it asked. It now goes with the passage through all of them (USER_RULES.this_request); the consistency check and the reduced context receive it too. -
The prompts name what they are sent and what a reader decided. The review and revision in one call and the arbitration, the two steps that rewrite after judging, had no word about
decided_by_reader, which the translation, the polish, the revisions and the other reviews have: a gender a reader decided could be “corrected” back there. The polish was sent the user rules, the locked glossary and the series conventions without a word about them; the final review itsTECHNICAL_CHECKSandPREVIOUS_CRITIQUES, and the final review’s revision itsTECHNICAL_CHECKS, never named. Each prompt now says what these sections are and how they bind; the arbitration is also told thatUSER_RULESoutrank every other evidence and that a proposal naming a paragraph is settled in that paragraph. -
A volume’s analysis no longer fails on characters merged in a chain. When the analysis joined a character to a second one, then that second one to a third, a name of the first still pointed to the vanished sheet: the consolidation stopped with a bare number as its error (“4”, “50”) and every later volume of the series waited on it. Every name now follows the merge. Relaunch the analysis of the volumes that failed this way.
-
A French translation no longer keeps the punctuation of the English it came from. On a translated chapter of a light novel, the machine wrote « Je le ressens vraiment, » s’exclama-t-il (the comma inside the guillemet, as in English), glued its dashes to the words of an incise (désormais—bien que—et) on a page that spaced the others, mixed « ?! » and « ? ! » in one scene and put a space before the ellipsis of a hanging word (« Ce petit chanceux … ! »). What the machine writes in French is now set as a French typesetter would set it: the comma leaves the closing guillemet before an incise, a dash glued to a word is spaced (not between figures, nor after an apostrophe), a group of ! and ? is written without a space (« ?! »), and the ellipsis is glued to its word. Like the rest of the typography, this applies to the machine’s text only, when it is saved and when a book written before is read out; a person’s text is kept as typed.
-
A thought the source does not quote is no longer turned into speech. A character’s thought, in italics without quotation marks in the book, came back from the model wrapped in guillemets (« Je suis… je suis une horrible personne… »), and the reader heard it said aloud. When a source passage holds no quotation mark in any script and no dialogue dash, the one pair of quotation marks the machine wrapped around the whole translated passage is removed, in any target language, on saving and in every output of a book written before. Two quotations, a quotation inside the passage and a person’s text are never touched.
-
English → French prompts name the mistakes a second light-novel chapter showed. Translated, reviewed and polished, the chapter still read « J’ai abandonné » for I abandoned her (the object gone: I gave up), « un appel à l’aide de son frère » for a cry for her brother’s help, « Ils avaient sûrement juste sorti », « troubrait », « écoutait cette confession […] se déversait », « celui en qui elle s’était éprise », « quand ils se sont retrouvés » in a passé simple narration, a pluperfect inside a line of dialogue, « J’ai l’air d’être en retard » for Looks like I’m too late, « s’excuser » for excuse herself, « gouvernante » for attendant, « à l’époque » for that same evening, « Toux ! » for Cough!. The French sheet now names the tenses of a passé simple narration and of the speech inside it, the grammar the eye skips (être with verbs of movement, an infinitive after a verb of perception, s’éprendre de, words that do not exist) and the article before parts of the body; the English → French sheet names the object pronoun English states, more false friends and calques, the serial comma and sounds named instead of written. Every operation that writes or judges an English → French passage receives them (about 1,000 tokens, served from the provider’s prompt cache). Reviewers are also told that Libris now sets dashes and the comma next to a closing guillemet itself.
-
The import estimate counts a file too long to be one chapter. Such a file, imported split at its headings (the usual form of a long web novel), was counted as zero words, zero passages and one chapter, so the estimate said the book fitted the quota whatever its size. It is now counted as the chapters it will be imported as.
-
An EPUB whose style sheet names its fonts or images in the wrong case exports again. A source edited with Sigil can keep
url("../fonts/ARIALUNI.ttf")in its CSS while the file sits inFonts/: a reader on a case-insensitive system finds it, EPUBCheck answersRSC-007and the export of the fully translated book was refused (“EPUBCheck signale un EPUB invalide”). At export, aurl()of a manifest style sheet that names no file of the book but exactly one file up to the case now points at that file. The text and the other references are not touched, and EPUBCheck remains a blocking gate. -
An accepted AI proposal no longer turns a model mistake into a protected human correction. When the model had to write the correction a person accepted (a free-form suggestion, applied in the background by Accept or Accept all), its paragraph was saved as a human correction without the locked glossary being checked: a paragraph that lost a locked term of the book, its series or a shared glossary was written, and the final review and the arbitration, which never touch a human passage, left the mistake in place for good. The rewritten paragraph now passes the locked glossary as a translation does: an answer that drops a locked term is asked again, and when it never keeps it the proposal is not applied, with the reason, and nothing is written. What the model wrote is saved as machine text (origin
accepted_proposal) that the later checks and passes still look at, and is never validated; a passage a person had already corrected stays theirs. -
The MCP tool
edit_termno longer unlocks a term it was not asked to unlock. It sent the whole term to the route the interface uses, which replaces every field: an agent that named only the source and the translation (the two fields the tool required) reset the rest to the defaults, so a locked term a person had decided was unlocked, lost its series override, its category and its description, and a proposed term was accepted, all in silence. The tool now reads the term first and changes only the fields it is given, likeconfigure_bookandupdate_series; onlybook_idandterm_idare required, a call that names no field is refused, andacceptedcan be set explicitly. The HTTP route keeps its full-replacement semantics. -
A paragraph the model gives back in the source language no longer counts as translated. In an English → French book, a model that returned an English paragraph word for word got the
unchangedwarning raised; the arbitration rejected it, the warning was closed and never raised again, and the book endedcompleted, its exportcomplete, the passage scored 98. The alert is now raised again after the arbitration for as long as the paragraph is the source text, as an error the text still carries is, and a passage whose translation is still the source text (an openunchangedoruntranslatedalert its current text still deserves, on a passage nobody validated) is a residual like a passage kept in the original: listed in the autopilot’s report with the check’s reason (the book endscompleted_with_residuals), in the delivery report’sresiduals, and counted as missing in the text exports, whose chapter is no longercomplete. The translation is still written — a name, a quotation or a line kept in the original on purpose are legitimate, and the five-word threshold of the check is unchanged — and translating the passage, resolving its alert or validating it settles it. -
Outside the autopilot, a polish or a revision the model keeps getting wrong no longer turns a good translation into an error. When a polish, a review or a revision still answered invalidly after its attempts and its repair — a polish that dropped a locked term every time, for instance — the passage whose valid translation was already saved was marked
error, never finished, and the job ended asking for recovery after six calls for nothing. The guard that refuses the faulty text is kept; the step is now given up instead: the translation already saved stays, the passage finishes with the warning “Step not applied after invalid model answers” (codeimprovement_skipped), and the decision log keeps the reason, as the autopilot already did. A provider outage, a refusal or missing credentials during these steps still stop the passage as before. -
Two locked terms that overlap no longer refuse the right translation. With “Dragon → Wyrm” and “Red Dragon → Dragon Rouge” both locked, « le Dragon Rouge » was refused for lack of « Wyrm », although “Dragon” was only said inside “Red Dragon”, and the prompt asked for both. A place in the source that a longer locked term covers now belongs to that term: the output check no longer asks for the shorter one there, and the prompt only offers the shorter one where the passage or its neighbours say it on its own.
-
A locked term in German, Spanish, Russian or another language is no longer satisfied by a word that only starts like it. Outside French and English, the check kept the first letters of the term and let the rest vary: « Schwein » and « Schwester » counted as « Schwert », « Espalda » and « Espadachín » as « Espada ». The whole word is now required, followed by the endings of its language: German cases, plurals and feminine (Schwertern, Mondbrunnens, Dunklen, Magierinnen), the number and gender of Spanish, Italian, Portuguese and Catalan (Elegida, Cavalieri, Maghi, Dragões), and elsewhere the stem without its final vowels and a short ending (Тёмного Лорда, Mieczem).
-
A volume translated into another language no longer receives the series’ French terms. A series decision was imposed on every volume of the series whatever its languages: an English → German volume was told, and checked on, « Moonwell → Puits-de-Lune », so the right German translation was refused; carrying a term change through the series rewrote the German volume too (« Die Noyau d’Or. »). A person’s series decisions now only apply to the volumes of the series’ language pair (primary subtags compared, as for earlier volumes), in the prompt, the output check and the Terms applied to this book card, and a propagation only rewrites the volumes of the pair of the volume it starts from.
-
A volume’s deliberate departure from its series no longer becomes the series’ convention, and a series decision beats an automatic term of the book. A term a volume marked Overrides series was handed to the next volumes as if the series had decided it, although the series glossary itself leaves it out; those volumes now keep the series’ convention. And a person’s series decision left unlocked lost to a term the analysis had proposed and the book accepted automatically, contrary to the documented precedence: it now wins over any book term that is neither locked nor marked as a departure, in the prompt, the output check and Terms applied to this book alike.
-
A JSON glossary is read with single spaces, and a blank source is refused. A JSON import kept ” Moonwell ” with its spaces (a term found nowhere in the book, and next to “Moonwell” as a second term), “Red Dragon” with two spaces and a no-break space, and accepted a source made of a space. As CSV and TBX files already were, and in the glossary forms of the interface and the API too, sources and translations are now kept without spaces at their edges and with single spaces; one left blank is an invalid row.
-
Carrying a term change through the translated text closes the alerts it answers. After « Pilule Dorée » was replaced by the locked « Noyau d’Or » everywhere, the passages that had been flagged for lacking « Noyau d’Or » kept their open alert, and the points it cost their quality score. The locked-term alerts of every passage the propagation rewrites are now computed again on its new text: closed when the term is there, restated when another locked term is still missing.
-
A correction saved before being validated, or made in a Chinese or accented passage, now reaches the next volumes. A validated correction tells the later volumes of the series the replacements it made (“Seigneur” → “Sire”) next to the names of the passage. Pressing Save then Validate in the editor recorded nothing, as the validation was compared with the saved correction instead of the machine translation; and a Chinese source or a name with an accented capital (« Élodie ») gave no name to recognise the passage by. The validation is now compared with the last machine version the person corrected, names are read in every script with capitals, and a source without capitals is recognised by the names and aliases of the book’s sheets. A passage corrected twice tells its replacements once.
-
The autopilot no longer undoes a person’s glossary decisions. A term a person deleted came back with the next analysis and was accepted again by the autopilot; a term corrected by hand but left unaccepted was deleted by the autopilot as if the analysis had proposed it; and a proposal “dragon” joined a locked “Dragon”. A person’s additions, corrections, imports and deletions of book terms are now written to the audit journal (
glossary.term_decided,glossary.term_removed) and read back: the autopilot never decides a term a person added, corrected or imported, the analysis never proposes again a term a person removed (a person can still add it back), and a proposal is compared with the book’s terms whatever its case. No migration. -
“What you keep correcting” now reaches the translation. Its button made a recurring correction a glossary term whose source was translated text (« sourit légèrement. »): a glossary term is only given to the model for the passages whose source says it, so the rule never reached a prompt, although the guide promised it held for the whole series. Make it a writing rule now adds the correction to the Required wording of the series’ style sheet (or the book’s, outside a series or when the series is not yours): every prompt receives it with the user rules (“Where you would write “sourit légèrement”, write “esquissa un sourire””), the volumes of the series’ language pair follow it, and the translated passages nobody validated that still carry the old wording go back to review. The style sheet lists the required wording and removes it.
[0.20.0] - 2026-09-26
Upgrade notes
- No migration, no change to the licence protocol.
/app/backend/appnow contains adjacent.pycfiles, not.pysources. Alembic’s path-loaded migrations remain.py. Scripts that read application source files from a running image must use the source checkout. Error tracebacks still name files and line numbers, but no longer display the lines of code.- At maximum quality, a passage whose review had already begun under the former order (a job paused or interrupted during the update) is finished without a polish: it is not polished and reviewed again.
- No migration, no change to the licence protocol. Built-in prompts move to
file-v5+rules-v3: the review prompts of every pair, and every writing or judging prompt towards French, change, so their requests are no longer served from the response cache of earlier versions. - No migration, no change to the licence protocol. Nothing is rewritten in the database: the typography below is applied each time a book is read out.
- For a book translated before 0.18, the
sha256of its chapters (ZIP manifest, automation API results) changes once, with its text. A result already stored for a finished automation request is still served as it was stored.
Changed
- Published application images no longer carry the Python source of
backend/app. The build substitutes the licence verification key before compiling the package in a separate stage, then copies only the compiled result into the final image. Image checks cover source absence, imports, command-line modules, the key and OpenAPI descriptions; the end-to-end suite still runs against the built image. - The review knows what to look for in English → French. A chapter translated, reviewed, revised and polished
in
maximumby the same 27B model came out with a double negation, a dangling gerund (“En marchant dans la rue, la pluie tapote”), calques (“C’est trop une coïncidence”), false friends (random → “aléatoire”, though → “d’ailleurs”), a colloquial narrator turning to the imperfect subjunctive — and no correction: “check grammar and register” names nothing, and a model does not see its own calques. The final review even “corrected” « Tellement. » into « Tellement ennuyé. ». Libris now appends a list of the mistakes a model makes without seeing them, with short examples, to the prompts that write or judge a passage: those of French whatever the source, then those of English → French (calques, false friends, anglicisms, the progressive aspect). Translation and revision avoid them, polishing corrects them, the review, fused review and final review look for each one and propose no correction that makes one, the arbitration accepts a proposal that removes one and never a correction that makes one. The user’s instructions and the style sheet outrank the list (a formal register chosen there allows the imperfect subjunctive). Other pairs receive nothing yet; Japanese → French receives the French part. About 450 tokens per writing call and 500 to 550 per judging call in English → French, in the fixed start of the prompt that a provider’s cache can serve (#180). - Reviewers leave French typography to Libris. Since 0.18 Libris sets French spacing, apostrophes and ellipses itself, yet the review prompts still asked for “target-language typography”: on one book, 9 arbitration decisions were about apostrophes. The reviewers of French text now never report or correct them and reject a point that only concerns them; the choice of quotation marks is still reviewed. Calques, false friends and unidiomatic phrasing are named as genuine problems, not stylistic preferences.
Fixed
-
At maximum quality, the polish can no longer change the meaning unseen (#179). It now comes right after the translation, before the review and the revision: they read the polished text and can put back a meaning it changed (« Eh, tant pis. » polished into « Eh, merde. »). It used to come last, read by nobody but the final review. Its answer is also screened before it is kept:
- a polished paragraph of four words or more that lost more than a quarter of them keeps its previous text (« Je m’ennuie. Tellement incroyablement ennuyé. » cut down to « Je m’ennuie. Tellement. »); when nothing the polish changed is left, no version is saved;
- a polish that adds an error of the automatic checks (a locked term lost, text left in the source’s script) is refused whole, as a revision of the final review is.
Every refusal is in the decision log (
translationstage,polishing,rejected). A review job no longer polishes, and the cost estimate follows the new order. -
A passage that leaves no room for its neighbours is translated in parts on a small window, like a passage too long, instead of failing with “too small to keep the passage’s neighbourhood”; the arbitration uses its reduced context in that case.
-
Books translated before 0.18 now leave with French typography, in the exports and in the reader. 0.18 typesets what the machine writes, but a finished book is never written again: a book translated under 0.17 kept its straight apostrophes — mixed with curly ones in the sentences the arbitration had rewritten — and ordinary spaces before ; : ! ?. Every output now typesets the machine’s passages as it reads them (
delivered_unitsanddelivered_textinapp/engines/typography.py): translated, partial and bilingual EPUB, TXT, chapter ZIP, Markdown, Word, the export of one chapter, the batch exports (a series’ texts or EPUBs), library publication, e-mail delivery and WebDAV, automation API results and the MCPexport_book, the integrated reader, proofreading links and the chapter preview. The editor, the correction fields of the reader and the project and series archives, which restore a database, keep the stored text. A person’s text and a retained original leave as they are; in Word, a corrected passage and the model’s version it is compared with keep the same typography, so no revision appears that nobody made. A language without rules is unchanged, and a book already typeset stays identical. (#184) -
The text exports of an EPUB volume keep their no-break spaces. Joining a paragraph onto one line took U+00A0 for an ordinary space, which undid the typography of books translated with 0.18 in TXT, Markdown, the chapter ZIP, the bilingual EPUB and the automation API results.
[0.19.1] - 2026-09-26
Upgrade notes
- No migration or request change. Licence server 0.12.0 sends
trial_already_used; older servers remain compatible.
Changed
- A website free trial refused on a machine or database that has already activated another website trial now shows a specific English message with the plans and support contact. The refusal does not invalidate a previously saved certificate.
[0.19.0] - 2026-09-26
Upgrade notes
- No migration, no change to the licence protocol: works with the licence server already deployed (0.11.0).
- A question asked about a passage now needs a valid licence; without one it answers
402 {"code": "licence"}.
Fixed
- A question asked about a passage now needs a licence, like every other model call. Asked from the editor, outside
any job, it reached the model without the licence being checked. It now follows the rule of a launch — a valid
licence, or a quota reached on a book whose words were already counted — and is otherwise refused with
402 {"code": "licence"}, the refusal a resumption gets.
[0.18.1] - 2026-09-26
Upgrade notes
- No migration, no change to the licence protocol.
AUTOPILOT_ESCALATE_AFTERandAUTOPILOT_ESCALATE_WINDOWno longer exist; left in a.env, they are ignored.
Changed
- Autopilot continues after invalid model answers. Exhausted translation attempts retain the best available
text (or the source, visibly flagged). Incomplete analysis, improvements, consistency checks, final reviews and
arbitrations no longer block later stages. The report and interface count retained sources, unresolved passages,
inconclusive checks and analysis gaps. API deliveries keep the warning status and
complete=falsefor those unresolved points; model unavailability still follows provider fallback and outage handling. - A passage the book’s model fails five times gets two tries on the stronger model, then goes back to the book’s model; the passages around it never leave it (the owner’s rule). Five refusals, answers that never validate or errors on one passage in a job — a call interrupted by a pause does not count — send its next calls to the stronger model, two at most, logged with the passage. This comes on top of the passages that come back to the arbitration and of the recovery ladder’s stronger rung.
- The stronger model is kept for the hard cases. After three failures in five calls of a step, it used to take over that whole step for the rest of the job — every translation of the book, or every review, at the stronger model’s price, and before the fallback providers. No step of the book moves to it any more. It arbitrates a passage that comes back to the AI arbitration (an earlier round arbitrated it and its points are open again: an answer that never validated, an error its text still carries) — a first arbitration is always the book’s model’s — and it still translates a passage no rung of the recovery ladder could. Each use is logged with its passage; a stronger model that is down sends the passage back to the book’s model. Settings, API and interface texts say so.
[0.18.0] - 2026-09-26
Upgrade notes
- No database migration or licence protocol change. Machine-written French text gets the new typography rules on its next rewrite; existing translations remain as stored.
- An autopilot job now blocks when translation, consistency, final review, or arbitration remains incomplete. Correct the provider or the remaining issues, then resume the job; completed work is retained.
Changed
- French typography is applied to what the machine writes. On 2,980 translated passages, 77 % had an ordinary
space before ; : ! ? or », 71 % after «, 542 after the dash opening a line of dialogue, and the books mixed straight
and curly apostrophes: the prompt asks for it, a model forgets it, and a reader’s line break then falls before a
question mark. Every machine write (
save_version) now sets a no-break space (U+00A0, the one every e-reader font has) before ; : ! ?, inside « » and after a dialogue dash, the apostrophe ’ and the ellipsis … (app/engines/typography.py). Only an existing space is made no-break, except inside guillemets; no word or inline marker changes. A person’s text is stored as typed; passages already translated change at their next machine write. - The typography rule sent with the prompts gives way to the style sheet: a sheet asking for dialogue dashes or English
quotation marks is no longer contradicted by “guillemets « » for dialogue” (prompt rules
rules-v2).
Fixed
- An alias in common no longer makes two characters one. The analysis attached a new character to any sheet sharing a single name with it, and merged sheets two of its names pointed to: Lily Potter was absorbed into Harry Potter through “Potter” (and took his gender, so every agreement about her was wrong), Queen Elara into King Aldric through “Your Majesty”. A character now joins a sheet only by its own name or by the sheet’s canonical name, in the written memory and in the parallel analysis alike; a name two sheets carry resolves to neither, the shared alias is proposed under “Identities to match?”, and sheets with two known genders are never merged without a person. The same applies to the characters of a Bible or of an analysis a person saves.
- The autopilot’s arbitration no longer closes a point it did not treat. An answer that decided nothing closed every open point as rejected and the passage left green; an accepted critique could leave its paragraph unchanged, another paragraph could be rewritten, and a paragraph cut down to the corrected phrase was applied. An answer must now decide every point once, change the paragraph of every correction it accepts and no other, keep 60 % of each corrected paragraph and never replace it with an instruction. An answer that cannot, after its attempts, leaves the points open for the next round instead of closing them, and an error still in the text (a locked term missing) is raised again after being rejected. Proposals now carry their severity and paragraph to the model.
- The fused review no longer undoes a change made during its call. Its revision was written on the passage reloaded after the model answered, so a term propagated meanwhile became the base revision and was overwritten by a revision of the old text. It is now written against the revision the model read, as the separate revision is; the stale one stays in the history.
- A passage translated in parts keeps the space between its sentences (small context windows, and the recovery rung that translates sentence by sentence). The parts of a paragraph were glued as the model returned them (“froide.Elle attendit”). They are joined by a space (a line break where the source broke the line) in languages that space their sentences, and touch in Japanese, Chinese or Thai.
- The reduced-context rung of the recovery keeps the style sheet and the chapter’s instructions. It sent only the book’s and the passage’s instructions, so a recovered passage (or an arbitration on a small window) could say “vous” where the sheet says “tu”, or leave the narrative tense the book uses.
- The style sheet’s dialogue check missed English web novels. A mark the source used was never held against the translation, so straight quotes kept from an English source were never reported. They are now, when they are the translation’s only quotation marks; next to the sheet’s own marks they still read as a quotation inside the dialogue.
- Replacing a term across a book, and placing translator notes, find it whether it is written with a straight or a curly apostrophe.
- Locked terms follow the grammar of every target language, not only French. The check, which refuses a translation (three attempts, then the passage fails), accepted only French endings: correct translations were refused in Korean (마법사는), Russian (Тёмного Лорда), German (Schwertern), Spanish (Elegida), Arabic (بالسيف), and in French itself (Gardienne, Maîtresse, Blanche, “Coeur” for “Cœur”). French and English now know their feminines and plurals (Sorcière, Actrice, Ladies, Elves), another language keeps the start of the word and lets its ending vary, and a term of an unspaced script is found glued to its neighbours. A one-character Chinese or Japanese term (王 is also in 王国, “kingdom”) is reported, never refused.
- Korean, Thai, Khmer and Burmese names are found in the text. A name followed by a particle (민수는) was never “mentioned”: its glossary entry, character sheet and series identity were left out of the prompt, the autopilot counted no occurrence of its terms, and locked terms were not checked. One definition of the unspaced scripts now serves the context, the checks and term propagation; a katakana name no longer matches inside a longer word (リン in リンゴ).
- Lengths are compared whatever the scripts. A translation’s length was compared character for character, so Chinese → French passages broke the bounds 93 % of the time (an alert, −15 and a review call each) and English → Japanese or Korean lost 5 points two times out of three, while an omission from Japanese went unseen. Lengths are now measured in letters of an alphabet (a Han character weighs 4, a kana 1.6, a Hangul syllable 2.3): on human translations, alerts fall to 0–0.4 % for every pair, English → French unchanged.
- Text left in the source’s script is reported (
untranslated, an error): Japanese left in a French translation, including a passage copied back unchanged, which the “unchanged” check missed for lack of spaces to count words. None was raised on 58,000 human-translated segments; an English novel quoting a Japanese sign keeps it. - A final review’s verdict survives the failure of its correction. When the reviewer found errors and the revision that should fix them failed (refusal, invalid answers), the whole review was dropped: the passage stayed “ok”, scored 100 and was never arbitrated, and with fallback providers the review was paid again on each. The verdict is now applied as it is — its points stay open for the arbitration — and the failure is logged with its reason, as is a review that fails everywhere (its reason used to be only the name of an exception).
- Autopilot completes each stage before starting the next. Unresolved translation, consistency, final review, or arbitration blocks the job with a reason. Review and consistency try configured fallback providers; resuming retries only unfinished work.
- The French name of the Personal plan reads « Personnelle » in Settings › Licence, as on the website and on the licence server.
[0.17.1] - 2026-09-26
Upgrade notes
- No migration. Nothing is counted again: the cycle’s counter stays what 0.17.0 left.
Fixed
- The import estimate promises exactly what the addition accepts. Past the quota, it no longer floors what is left at
0 before adding the margin (quota 100,000, 110,000 used: it announced 20,000 words instead of 10,000), and it no longer
ignores the account’s own monthly allowance. One computation now serves both (
app/licence/charge.py,room); the estimate returnsroomandaccount_room, and the assistant says which quota refuses. - A cycle its carry-over emptied limited additions wrongly: its quota shows 0, and a book was accepted as if there were no limit. Only the margin is left then.
- Refusal messages: numbers are written the same way for the licence and for the account.
librisctlrecognises an installation made by the installer (/opt/librisand theLIBRIS_PROJECTof its.env) when the old/opt/libris-productiondoes not exist.- Documentation: the API guide (EN/FR), the OpenAPI description (
allowance_insufficient, 402 at the import), operations and architecture describe the 0.17 counting at the addition; the CI comment about CT117 is corrected.
[0.17.0] - 2026-09-26
Upgrade notes
- A book is counted as soon as it is added. No migration, and nothing is counted again at the upgrade: the cycle’s counter goes on from the number it had. The books already there keep being counted as before, passage by passage as they are translated; only books and chapters added afterwards are counted at once.
Changed
- A book’s words are counted when it is added, no longer passage by passage as it is translated: all of
its words, even if it is deleted afterwards without being translated. This holds for the import assistant,
the automation API, the MCP server, source watches, archive restores and second languages; a chapter added
or replaced later counts only its new passages (
app/licence/charge.py). - A book is accepted while it fits in what the cycle’s quota has left plus the margin
LICENCE_QUOTA_OVERRUN_WORDS(20,000 words); beyond, it is refused whole (402 licence_quota_insufficient, orallowance_insufficientfor an account’s monthly quota) and nothing is counted or created. The import estimate says so before the import (margin), and the assistant then disables its three import buttons. - A book already counted is translated, launched again and corrected without costing anything more, even once the quota is reached; only a provider comparison, which calls the models on top, is still counted and stopped by the quota.
[0.16.4] - 2026-09-25
Upgrade notes
- The images move to
registry.libris-translate.com/libris/libris. The GitLab project left thedevgroup for the newlibrisgroup, and the registry follows the project:registry.libris-translate.com/dev/librisno longer serves any image. Update with the installer from the website (curl … https://libris-translate.com/install.sh | sudo bash): it pulls from the new path and rewrites the pinnedLIBRIS_IMAGEandLIBRIS_CODEX_IMAGEof.env. The registry credentials of a licence are unchanged. An installation that pinsLIBRIS_IMAGEby hand, or a source checkout withLIBRIS_REGISTRY, must name the new path itself.
Changed
- New image path. The installer, the Compose file,
.env.exampleand the documentation nameregistry.libris-translate.com/libris/libris(:latest,:X.Y.Z,:codex-X.Y.Z).
Fixed
- An update no longer stops on an installed image that no registry serves any more. The installer used to
fail with “could not download the installed image” when that image was neither on the host nor downloadable
(the old
dev/librispath, a pruned image). It now comparesdocker-compose.ymlwith the new template only: an identical file is replaced, any other is kept, the template is written beside it asdocker-compose.yml.libris-new, and the same command resumes once it is merged.
[0.16.3] - 2026-09-25
Upgrade notes
- The Compose project is renamed
libris. The next update with the installer stops Libris for the time it takes to copy theepub-translator_*volumes (database, books, Codex state, journal) intolibris_*twins, checks each copy, removes the old containers and network, and recordsLIBRIS_PROJECT=librisin.env. It needs free space for one more copy of the data; without it, the installer keepsLIBRIS_PROJECT=epub-translatorand says how to retry. The old volumes are never removed: the installer prints thedocker volume rmcommand for when the new name has proved itself. A source checkout updated withscripts/deploy.shkeepsepub-translatoruntil its.envnames a project.
Changed
- No more
epub-translatoron a new installation. The containers were alreadylibris-*, but the Compose project, its network and its volumes still carried the name of the original prototype. They are nowlibris,libris_defaultandlibris_database,libris_books,libris_codex-state,libris_logs; the installer and.env.examplewriteLIBRIS_PROJECT=libris, the Python package islibris, and local images are taggedlibris:localandlibris:rollback. The OpenViking root (viking://resources/epub-translator) is unchanged: it addresses documents already published.
[0.16.2] - 2026-09-25
Upgrade notes
- No database migration. Nothing to do beyond the usual update; the final review now keeps corrections that improve a passage, so a book reviewed again may see more passages rewritten (still listed “to check” when a remark remains).
Changed
- The final review keeps a correction that improves a passage, not only a flawless one. A revision was
applied only when the second reading flagged nothing at all, so any remaining nitpick threw the whole
correction away: on a 17-volume series, 40 revisions that removed errors were discarded in one run and
their passages stayed “to check” unchanged. A revision is now applied when it adds no error of the
automatic checks and leaves fewer errors, or as many errors and fewer other remarks; what the second
reading still flags stays on the passage, still to check (
app.engines.translation.final_review.improves).
Fixed
- Irregular plurals of a locked term are recognised. “Evil Eyes” translated “Yeux maléfiques” failed the locked term “Evil Eye → Œil maléfique”, and the correction the review asked for was refused as “locked glossary not respected”: no translation could satisfy both. The check now accepts the irregular plurals œil → yeux, ciel → cieux, aïeul → aïeux, and -al → -aux (“Général” → “Généraux”).
[0.16.1] - 2026-09-25
Upgrade notes
- One database migration,
59edaaf5b59b: it only marks stale the passage scores that counted failed model calls or arbitrations; they are recomputed the next time the book’s quality or editor is opened. Back up the database first, as for every release with migrations. - The displayed name becomes « Libris Translate »: window and tab titles, e-mails, the OpenAPI title.
The issuer of authenticator codes (TOTP) and the default WebAuthn name (
WEBAUTHN_RP_NAME) change for new enrolments only: codes and keys already registered keep their old label and keep working; an explicitWEBAUTHN_RP_NAMEin.envis kept. Technical identifiers (libris,LIBRIS_*, images,librisctl) do not change. - Licence server 0.8.1 or later is recommended: the refusal messages follow its codes, and an installation that never signed enrols its key when it requires signatures. With an older licence server, unknown codes show the server’s own message.
Changed
-
A corrected passage is no longer ranked below an untouched one. The passage quality score stopped charging a passage for its history: model calls that failed before a translation passed the checks (
retry, was 3 points each, up to 12) and critiques the arbitration applied or accepted (arbitration, was 3 points each) now cost nothing; a rejected (2) or deferred (4) critique still does. On a 17-volume series, 150 of the 234 passages outside the good band were there only for these reasons, although the autopilot had corrected them. Both signals stay in the stored list. -
Documentation: consistency with the licence server and the website. The release procedure in
docs/development.mdno longer describes a deployment at the tag: a tag publishes the images and the GitLab release and deploys nothing; customers install and update fromregistry.libris-translate.comwith the installer, and the website’s/version.jsonis what the licence server announces.docs/operations.mdsays the same of the production deployment script. -
What a plan grants (
docs/configuration.md#licence, and its French twin): accounts, machines (a place freed after seven days,move_too_soon), automation API and sharing (Studio and Pro only,402 automation_not_licensed/sharing_not_licensed), the quota of the cycle; the per-account word limit stays per calendar month; the certificate is renewed every hour and lasts 72 hours. -
The Docker guide (EN and FR) gains an Activate the licence step and lists the Codex bridge the installer starts; troubleshooting rows for the licence refusals in
docs/operations.md. -
Help goes to support@libris-translate.com; security reports still go to licences@libris-translate.com with
[SECURITY], acknowledged within two business days (SECURITY.md). The product is named Libris Translate at its first mention. -
.env.example: comments follow the quota cycle, and the SMTP examples no longer name real hosts. -
The product is named Libris Translate wherever it names itself (the owner’s decision; the website already does). Rule: the full name in every title and every first mention — window and tab titles (
… · Libris Translate), the sidebar and phone header (« Libris » with « Translate » set beneath it), the sign-in page, the loading screen, the update banner, the “Leave” and “users” card titles, the e-mails (subject lines, first mention, signature « — Libris Translate »), the FastAPI and OpenAPI titles (“Libris Translate automation API”,docs/openapi/libris-v1.jsonregenerated) and the MCPserverInfo.titleand instructions. « Libris » alone stays accepted afterwards, in a text on a screen that already shows the name. The defaultWEBAUTHN_RP_NAMEand the issuer of new authenticator codes become « Libris Translate » (keys and codes already registered keep working). Technical identifiers do not change:dev/libris,librisctl,LIBRIS_*,X-Libris-*,.libris-*, image and export file names, the MCPserverInfo.name(libris), i18n keys.
Fixed
- A locked term in the feminine is no longer reported missing. The locked-glossary check accepted the
plural of a locked translation but not its feminine: “Souveraine de la sagesse” failed “Ruler →
Souverain”, raised an error alert, and a correction written that way was refused as “locked glossary not
respected”. The check now also accepts the
-eagreement. - Licence refusals say why, in the interface’s language. The activation answer waited for codes no
licence server ever sent (
revoked,expired,too_many_instances) and fell back on “refused” for the real ones. Every code of the licence server 0.8.1 now has its own message in English and French (app.licence.REFUSALS): unknown key, revoked, suspended or expired licence,move_too_soon, the signature refusals (signature_required,bad_signature,malformed_signature,no_public_key,replayed,clock_skew),too_many_attemptsandregistry_unavailable. A code this version does not know yet shows the server’s own message in French, and names the code in English rather than showing French words. Settings › Licence gives the reason of a licence refused at renewal instead of “refused”, and shows the last refusal of a licence still in force (a clock that is off, a report received twice). Which refusals cost the certificate (INVALID) does not change. - “Monthly word quota reached” becomes “This cycle’s word quota has been reached.” (FR « Quota de mots
du cycle atteint. »). The
stop_reasonof the jobs it pauses stayslicence, and a job stopped by an earlier version keeps reading in English. The notification setting now says “At 80% of the cycle’s quota”. - The plan is shown by its name in Settings › Licence: Trial, Personal, Studio, Pro (FR Essai,
Personnel, Studio, Pro) instead of
trialorpersonal; a plan this version does not know keeps its identifier. - The refusal of an account past the licence’s seats gives support@libris-translate.com instead of “write to us”.
- An installation that never signed enrols its key when the licence server requires signatures. A
Libris activated before signing existed (0.10 or older) holds no signing pair — a heartbeat never draws
one — so its reports go unsigned; on a licence server with
SIGNATURES_REQUIRED=trueevery renewal answeredsignature_required, and the installation stopped once its 72-hour certificate ran out. A renewal now answerssignature_requiredas it already answeredno_public_key: one activation with the saved key, which draws and enrols a pair. If that activation is refused, its own reason is kept (a revoked licence still costs the certificate;bad_signature, when the server holds the key of a pair lost with a changedSECRET_KEY, keeps it and says to contact support), never a loop. - A request the licence server could not read (FastAPI’s 422, whose
detailis Pydantic’s list of errors) was stored raw and shown as such in Settings › Licence. It is now namedinvalid_request, with a readable message (FR/EN) and the fields it mentions; it still never costs the certificate (INVALIDunchanged). - The
move_too_soonrefusal gives the licence server’s rule: a machine gives up its place only after 7 days on the licence. - “Quota of the month” becomes “quota cycle” where it is the licence’s: the
licence_statusMCP tool’s description, the per-account words hint in Settings › Queue (that limit itself stays per calendar month), and code comments. - Documentation (
docs/configuration.md,docs/configuration.fr.md,docs/user-guide.fr.md): “one licence, one installation” and “twelve places” give way to the machines of each plan (trial 2, Personal 3, Studio 5, Pro 10) and the 7-day rule for moving; the user guide quotes the currentmove_too_soonmessage; a perpetual certificate has no word limit rather than “no monthly limit”. - Messages name the screens as the interface does: “No licence activated. Enter your key in Paramètres › Licence” (was Réglages), the per-account quota refusal points to Paramètres › File d’attente, and the MCP server’s documentation to Mon compte › Jetons d’API. Jobs a 0.16.0 stopped with the old wording still read in English. A dead translation key (“Tester la connexion”) leaves the providers screen.
SECURITY.md: the supported versions are the latest release and the one before it, as its text and the website already said (the table said the latest only).
[0.16.0] - 2026-09-25
Upgrade notes
- Two database migrations:
d0d68575c19awidenslicence_usage.periodto name a quota cycle (YYYY-MM-DD) as well as a calendar month, ande7c1a4b9d352adds three columns towebdav_connectionsfor automatic sending. Back up the database first, as for every release with migrations. Downgrading the first one deletes the rows of cycles (the licence server keeps its own count). - Quota cycles need licence server 0.8.0. With it, the word quota renews on the subscription’s anniversary and an overrun is taken from the next cycle; with an older licence server nothing changes: calendar month, no carry-over. Update Libris and the licence server in any order.
- A book that does not fit in what is left of the quota is refused before it is queued
(
licence_quota_insufficient,402). Automation scripts that start books through the API or MCP should handle that code; the book stays imported and can be started once the next quota opens. - The perpetual certificate of the end of activity is accepted from this release on. Nothing to do today: it would only be sent if the publisher ended its activity, and it is installed in Settings › Licence › Perpetual certificate.
- Nextcloud / WebDAV automatic sending is off for every existing connection: turn it on per connection in My account › Nextcloud / WebDAV connections.
- Trial and Personal licences lose the automation API, MCP and sharing (see Security below); tokens and
links are kept. Run the installer again rather than
docker compose pullalone: an installation left on an older Compose file is now reported asoutdated(#163).
Added
-
The perpetual certificate of the end of the publisher’s activity is accepted (#177, dev/libris-licence#29). If the publisher ends its activity, every valid licence receives a certificate signed with the same key as the hourly one (
v: 2,kind: perpetual,reason: end_of_activity), with no end date, no machine and no monthly word limit, bound to the licence key bykey_sha256(the digest of the key as the licence server normalises it). Libris accepts it only with the licence key it was signed for — typed in any case, with or without dashes — and on any machine, so a reinstallation or a restored backup keeps working. An administrator installs it in Settings › Licence › Perpetual certificate (paste it or choose the.libris-certificatefile, and give the key unless it is saved), withpython -m app.licence.perpetualin the API container, or withPOST /api/settings/licence/certificate; it is written to the journal. From then on nothing is asked of the licence server any more (no renewal, report or release), no answer can remove the certificate — not even a revocation already on its way —, work is allowed without an end (« Perpetual licence (the publisher has ended its activity) »), no monthly quota is applied and no late-renewal warning is sent; accounts, the automation API and sharing follow itsfeaturesas with the hourly certificate. Activating another key still replaces it. Any otherv: 2is refused, and av: 1certificate without an end date is still expired.GET /api/settings/licencegainsperpetual. Cross-repository check:scripts/audit_perpetual_contract.pyreads, in separate processes, what the licence server’sscripts/end_of_activity.pysigns. -
The word quota follows the subscription’s cycle, and what a cycle overran is taken from the next (dev/libris-licence#32, #33). With a licence server that counts in cycles, the quota renews on the subscription’s anniversary rather than on the first of the month, and the words translated past a cycle’s quota — the overrun that lets a running book finish — are taken from the next cycle, once. Libris counts its words in the cycle the certificate names, reports the last words of an ended cycle to that cycle, and applies what the cycle really allows. Settings › Licence shows the words of the cycle, the date of the next quota and what was carried over. With an older licence server nothing changes: calendar month, no carry-over. The heartbeat and the activation now send
period, which an older server keeps as an unknown field, signed like the others. -
A book whose words exceed what the quota has left does not join the queue (#176). Whatever the road — import assistant, book page, series start (each volume must fit in what the ones before it left), automation API, MCP, source watches — a book whose words still to translate are more than what the cycle has left is refused before it is queued, with a clear message and the stable code
licence_quota_insufficient(402onPOST /api/projects/{id}/jobsand the MCP toolstart_job;stop_reasonof a failed API request;codeof a refused volume of a series start). It can still be imported and analysed. The import assistant says so with the date of the next quota and disables Import and run the whole pipeline. Only the passages not charged yet count, so a volume that received new chapters costs only those; the 20,000-word overrun finishes a book, it never admits one; a licence without a limit never refuses. -
Administrators are told when a newer Libris is published (#166). The licence server may announce the latest release in an optional
latest_versionfield of its activation and hourly heartbeat answers (licence server withLIBRIS_LATEST_VERSIONset); nothing new is sent to it. When that release is newer than the one running, administrators — and nobody else — see a discreet banner: « Libris X.Y.Z is available », a link to the changelog on libris-translate.com, the installer command (and the one pinned withLIBRIS_TAG=X.Y.Z), and a reminder to back up and pause the books in progress first. Each administrator can hide it for that release; the next one is announced again. Nothing is ever updated automatically. An older licence server, or one that cannot be reached, announces nothing. API:GET /api/installation/update,POST /api/installation/update/dismiss. -
Coloured code blocks, one for the whole interface (#175). The built-in documentation and the screens that show a command, a configuration or a JSON answer (API example of My account, Codex update command, connection and memory checks, import validation report, character merges, model request details) share one code block: an ink panel in both themes, the language named, a Copy button that copies the raw text, keyboard scrolling, and the colouring of bash, JSON, YAML, Python, HTTP and dotenv/INI/TOML. The colours are the
--syntax-*tokens shared with the website, each at WCAG AA on the code background in Papier and Nuit, and give way to the system colours in forced-colours mode. The colouring draws tokens as elements, never HTML, and needs no new dependency. Inline code has the same look everywhere and long values (tokens, addresses, fingerprints) wrap on a phone; a code block indented under a list item of the documentation now shows as a block. -
OpenViking outages are visible, and failed events come back on their own (#165). Settings › Memory · OpenViking has an OpenViking status card: reachable or not (the worker checks every minute), the events waiting and in error, the last and most frequent errors, the next attempt, and Retry the failed events. A change between reachable and unreachable is written once to the journal; when OpenViking is back, its failed events are queued again at once. The same state is the
openvikingelement ofGET /health, for an administrator or for everyone withPUBLIC_HEALTH_DETAILS=true, like the configuration checks; an OpenViking outage never fails the health check. -
Optional daily prune of orphan vector records in OpenViking (#165). Next to the daily orphan scan, off by default and a dry run first:
reindex mode=prune_orphans(OpenViking ≥ 0.4.21) on the Libris root, result in the card and in the journal. An older server is noted once and not asked again until the setting is saved. -
Nextcloud / WebDAV: every finished book sent automatically, missing folders created (#125). A WebDAV connection (My account › Nextcloud / WebDAV connections) has a new option, Automatically send every finished book to this connection, with its folder and format: when the translation or review of a book the account owns finishes, the worker sends it there as the book page’s Send to Nextcloud/WebDAV would (same queue, same retries, listed under Previous sendings, a book finished again replaces its file). Off for every existing connection; a book shared with the account never leaves on its own. A destination folder that does not exist is now created with its parents (
MKCOL), inside the connection only. The connection list no longer squeezes a connection’s name and address to nothing on a phone. Migratione7c1a4b9d352adds three columns towebdav_connections. Google Drive, Dropbox, OneDrive and Mega stay out of scope (registered OAuth applications, no standard API for Mega): see the issue for what they would need.
Fixed
-
Settings › Licence shows its dates in the right year. The certificate’s end, the licence’s end and the last contact were multiplied by 1,000 twice and read as dates tens of thousands of years away.
-
Statistics: the “By model” and “Failures by cause” cards no longer touch (#171). Every block of the page now shares the spacing of the rest of the interface, with or without data, on a desktop and on a phone.
-
The export table in
docs/architecture.mdno longer reservesepubfor EPUB sources (#173). Any other source, or a chapter range, gets a reading EPUB written by Libris; only the rebuild of the original needs an EPUB source. The table also listsdocx, and a documentation test keeps it in step with the export route. -
Fixed-layout EPUBs are refused at import instead of being translated into an unusable book (#174). The documentation said they were not supported, but nothing detected them: a comic, a picture book or a laid-out textbook was imported, translated, and its word quota spent. An EPUB whose package declares
rendition:layout=pre-paginated(for the whole book or on every text page of the spine), whose Apple Books options setfixed-layout, or which carries Kindle’sfixed-layoutmeta is now refused with a reason in the interface language, in the import wizard, on the direct upload and in the automation API (422 fixed_layout_epub). When only some text pages are fixed, the book is imported with a warning that says how many; a fixed cover or plate without text changes nothing. A volume restored from a Libris archive is still accepted. -
An update that kept an older Compose file is no longer silent (#163). Updated by
docker compose pullalone, an installation ran the new images under the olddocker-compose.yml: without/logs, the journal switched itself off and said so once in the container’s output. The API and the worker now compare what they mount and write with what the Compose file shipped in their image expects (/data,/etc/machine-id,/logs, a journal that is written) and give one state,ok,outdatedordegraded(a journal that cannot be written), with each difference and the command that fixes it: run the installer again, or mergedocker-compose.yml.libris-new. Administrators see a banner and a new Settings › Installation health tab;/healthanswers the state, and its details to an administrator or withPUBLIC_HEALTH_DETAILS=true;librisctl doctorfails on a degraded configuration and warns about an outdated one. -
Series consistency no longer reports case as a contradiction. Translations are now compared the way source terms already were: “Fil d’araignée” and “fil d’araignée”, “RÉINCARNATION” and “réincarnation”, straight and curly apostrophes are one choice. Each remaining choice is shown and applied in the spelling the series uses most (on a tie, the earliest volume’s).
-
A Codex bridge left on another version is now reported (#164). The API reads the bridge’s version from its
/health(which now gives it, behind the same token) and compares it to its own; the answer is cached for five minutes. A difference shows in the LLM providers settings and on the Codex connection, ascodex_bridge: "mismatch"in/health(the versions themselves are for administrators, atGET /api/providers/codex-bridge), and as a failedlibrisctl doctorcheck, with the command that updates it:COMPOSE_PROFILES=codex docker compose pull && COMPOSE_PROFILES=codex docker compose up -d, or the installer run again. A bridge that does not answer isunknown, never an error. -
The quota warning is said once a month in every time zone. The start of the month it is counted from came out an hour late when the server ran in some time zones (Africa/Casablanca), so a warning sent in the first hour of the month could be sent a second time. It is now midnight UTC wherever the server runs.
-
CI/tests: no test depends on the hour it runs at (#167).
scripts/clock_check.shreplays the backend suite with the clock moved to midnight UTC, the end of the month and of the year, the changes of time and time zones on either side of the date line; aclock-checkjob runs it in a pipeline schedule that setsLIBRIS_CLOCK_CHECK(created by hand, seedocs/development.md). The work-window tests use a fixed day.
Security
-
Warning for Trial and Personal installations: those that used the automation API, the MCP server or sharing lose these features when they update; existing tokens and links are kept and come back with a Studio or Pro licence.
-
The automation API and sharing follow the licence (#172). The licence server has always signed
automation_apiandsharingin the certificate (Studio and Pro: yes; Trial and Personal: no), and Libris ignored them. Visible change for Trial and Personal installations: API tokens can no longer be created,/api/v1/*andPOST /mcpanswer402 automation_not_licensed(after the token is checked, so a missing or wrong token still gets its401); a new book member cannot be invited and no review link can be made (402 sharing_not_licensed), and existing review links answer402instead of opening the book. Nothing is deleted: tokens, members and links stay listed and revocable, members already invited keep their access, the owner keeps every book, and everything works again as soon as the licence includes the right. The rights are read from the certificate held for this machine, even an expired one (an outage of the licence server takes no right away); no certificate grants none; a certificate that does not name a right (a licence server older than it) keeps it, as before. The interface hides what is not included, with the reason, and Settings › Licence lists both rights.GET /api/settings/licencegainsrights. -
Every route that takes a provider id now applies the provider list’s own rule (#178). The estimate before an import (
GET /api/imports/{id}/estimate) listed every provider of the installation — the private providers of the other members and the retired ones included — with their name, model and estimated cost; it now forecasts only what the account may start a book with (its own providers and the shared ones of the installation; everything, for an administrator), never a retired one, and aprovider_idit may not use answers the same404as an unknown one. The same flaw, which could also spend another member’s key, is closed where a provider is chosen: confirming an import (provider_id,escalation_provider_id,fallback_provider_ids→422 Provider inconnu.), a series’ defaults (POST/PUT /api/series, a provider the series already names is left alone), a comparison of providers (POST /api/projects/{id}/jobs,operation: compare) and the automation API (pipeline.provider_id/provider_id→422 unknown_provider, the listGET /api/v1/providersalready gave).
[0.15.2] - 2026-09-24
Fixed
-
A plain
docker compose pullnow updates the Codex bridge. The bridge is a Compose profile, so a manualdocker compose pullorupleft it on its old image while the application moved on. The installer writesCOMPOSE_PROFILES=codexinto.env, and an update adds it to an older.env; a value the operator set is kept. -
e-Soleau presentation. Without
--with-history,PRESENTATION.mdno longer explains how to restore a Git history the deposit does not contain.
[0.15.1] - 2026-09-24
Added
- The files of an e-Soleau deposit for a release (
scripts/esoleau_bundle.py, manualesoleau-bundlejob on release tags). From the tag’s Git objects alone: the source archive, optionally the Git history as a bundle, a SHA-256 manifest of every file and a presentation of what is and is not part of the work, ready to upload to the INPI (1 to 100 files, 50 MB at the base fee).
Changed
- Licence and documentation.
LICENSE, the README and the development guide describe Libris as the proprietary software it is, and nothing else; the code of conduct, written for public contributions Libris does not take, is removed. Theesoleau-bundlejob deposits the source of the tag without its Git history.
Fixed
- The images carry their licenses. The application image now ships
LICENSEandTHIRD_PARTY_NOTICES.mdin/app, and the full license text of every package the interface bundles in/app/frontend/dist/THIRD_PARTY_LICENSES.txt, generated at each build; the Codex bridge image ships them in/service, with the license and NOTICE of Codex and the license of its Node.js runtime. MIT, ISC, BSD and OFL all ask for their notice to travel with the copies, and the bundle had removed most of them. The CI checks that each file is present.
[0.15.0] - 2026-09-24
Added
-
A journal to reconstruct an incident from one place (#156). The API, the worker and the migrations each write their events to a file of their own — besides the usual container output — in a
logsvolume, or in/opt/libris-production/logson a host deployed bylibris-production-deploy, which writes the steps of every deployment there too (deploy.log).librisctl journalmerges them into one chronology: the last events,-fto follow,export --since 2h --output FILEfor a period (--with-containersadds the database and Codex output), filtered by service, level, request or job. Each line gives the time with its UTC offset, the level, the service, the process, the request id (X-Request-ID, now returned by every response) and the job id; tracebacks keep their frames and their chain of causes on indented lines. Process starts and stops, API requests, job starts (resumed=yes), completions and failures and the migrations run are recorded. Passwords, cookies, tokens, licence keys, credentials in URLs and the configuration’s secrets are masked — in the container output too — line breaks are escaped so nothing forges an event, and messages that may quote a book or a prompt are never copied. Files are private (0700/0600), rotated atLOG_MAX_MB, keptLOG_BACKUPSdeep andLOG_RETENTION_DAYSlong; a journal that cannot be written never stops a service and says so once. See docs/operations.md#journal and theLOG_*settings. -
Every image carries its own installer (#145).
/app/deploy/install.shand the configuration template/app/deploy/env.examplenow travel beside the Compose file. Afterdocker pull, a customer can install or update from the image alone, with no archive and no Git. Whatever copy is started (the site’s bootstrap or one taken from an image), it resolveslatestorLIBRIS_TAGto a digest and hands over to the installer of that digest, so the installer, Compose file and template all come from the image that runs. A first installation writes its0600secrets in a staging directory renamed into place in one step, never overwrites an existing installation, and resumes after a failed start; an emptyLIBRIS_HOMEis now filled instead of receiving a nested directory.
Changed
-
Codex bridge: Codex CLI 0.154.0 → 0.156.1, so that the ChatGPT-account model list offers GPT-6 Luna (
gpt-6-luna) and GPT-6 Sol (gpt-6-sol). The list is still read from the app-server (model/list); the protocol changes between the two versions are additive only. Rebuild or pull thecodex-*image to get them. -
Ruff lints the Alembic migrations too (#155). The
backendCI job ranruff check app tests, so the migrations, which run in production at every upgrade, were never linted; it now runsruff check app tests migrations, and the ten unsorted import blocks this revealed are sorted (no change in behaviour). The development guide gives the same command.
Fixed
-
Memory events are written to OpenViking as vectors only: no more regeneration of the folder summaries on every write, which multiplied the vectors and the model calls (one event produced about 600 vectors; a CT114 store reached 1.9 million records for 6,000 indexed). Libris never reads those summaries. Servers that reject
processing_modestill get the plain write, as before. -
The translation of a volume no longer starts before its analysis is complete (#161). Several paths let a book be translated while the analysis was short of 100 %, so the glossary, the characters and the book’s memory were not kept consistent:
- under the autopilot, a passage whose analysis the provider refused or kept answering badly was skipped, and the translation started as long as half of the passages were analysed;
- a Book Bible synthesis given up left its section out: in the strict mode the section stayed « to consolidate » while the book was translated, and in the parallel mode it was even counted as consolidated, as were the chapters of a merge given up;
- the autopilot validated (froze) the Book Bible as soon as 80 % of the passages were analysed, a section missing or not;
- a
translatejob (the Translate button or menu, a batch action, the MCP, the API, a held job resumed after chapters were added by a followed source or an import, a copy for a second language taken while the original was being analysed) only checked that the Book Bible was not empty and translated passages that had no analysis at all.
Now the analysis asks again, in the same job, for everything it gave up and for passages that arrived meanwhile — once on the job’s provider, then once on each autopilot fallback provider (logged as
analysis_catch_up); a translation first analyses the passages it covers that have no analysis; the Book Bible is validated only when no section is left, and a catch-up never reuses a synthesis made for other chapters. What still cannot be analysed stops the job asblockedwith the newstop_reason = analysis_incompleteand a message saying what is missing, instead of translating a single passage.ANALYSIS_MIN_COVERAGEnow defaults to1(was0.5); lowering it is the operator’s explicit way to accept translating with gaps. The progress shows a translation completing its analysis as the analysis stage, the book stats carryanalysis_complete, and the book page says when a book already translated has an incomplete analysis, with a button to complete it.
Security
-
The example API client no longer hands its token to another server through a redirect (dev/Libris_Web#9).
examples/libris_client.pyrelied on urllib’s default redirect handling, which copiedAuthorization: Bearer …to whatever origin aLocationnamed. It now follows a redirect only within the origin ofLIBRIS_URL(same scheme, host and port, at most five in a row), never downgradeshttpstohttp, and refuses any other redirect with the error coderedirect_refused, so neither the token nor an uploaded book reaches another destination. A body is repeated only by 307/308 within that origin. Tested with real loopback servers for 301, 302, 303, 307 and 308, other ports and hosts, relative redirects and chains, on Python 3.10 to 3.14. -
A member’s own provider can no longer make the server call its internal network (#157). With
members_may_add_providerson (the default), any member could point a provider of their own at a loopback or LAN address and have the backend fetch it (POST /api/providers/{id}/testreturned what the internal service answered). A provider added by a member now only reaches public addresses: a name that resolves to a loopback, private, link-local, shared (CGNAT), multicast or reserved address — IPv4 or IPv6, IPv4-mapped and NAT64/6to4 forms included — is refused when the provider is saved (HTTP 422) and at every connection: test, translations, retries, batch submission, polling and results. The name is resolved for each request and the connection goes to the checked address (no second resolution a DNS rebinding could change), the certificate still verified against the name; redirects are not followed and proxy variables are ignored. The installation’s own providers, set up by an administrator, are unchanged: a vLLM or Ollama on the LAN keeps working. Upgrading: a member provider already aimed at a local model stops working; an administrator either adds that model as an installation provider shared with members, or opens its address to members with the newPROVIDER_MEMBER_PRIVATE_NETWORKS(CIDRs, e.g.192.168.1.40/32). The address checks of webhooks and WebDAV now share the same code, and judge an IPv4 address wrapped in NAT64, 6to4 or Teredo by the IPv4 address it carries. -
Single-use sign-in proofs are spent once, even by two parallel requests (#154). A pending sign-in, an authenticator code, a recovery code, a security key’s challenge and signature counter, a password-reset link and a single sign-on request were read as valid and marked spent later: two requests racing with the same proof could both pass (two sessions from one second step, one code or one reset link used twice, two assertions checked against the same counter). Each is now consumed by one database statement only one transaction can win (
DELETE … RETURNINGor a conditionalUPDATE), the account row is locked first — the order account revocation already takes — and the loser is refused. PostgreSQL tests race two real requests on each path.
[0.14.1] - 2026-09-23
Changed
- The volumes of a series run one at a time, in reading order (#153). A launched series no longer spreads the provider over all its volumes at once: volume 1 starts first, whatever order the volumes were launched in, and takes the provider’s whole share for its passages side by side — eight analyses of one volume instead of one on each of eight volumes; the next volume starts when it is done. Other books and series still share the provider as before. The queue says so for a volume waiting for its turn.
[0.14.0] - 2026-09-23
Changed
- Libris has a visual identity of its own. The interface drops the default indigo for a book-workshop palette — paper, blue-black ink, Prussian blue for actions and one vermilion “rubric” kept for landmarks — in both themes (“Papier” and “Nuit”). Titles, series names and figures are set in Fraunces, books in Source Serif 4, the interface keeps Inter. A new mark, the owner’s own artwork of an open book with a serif L on one page, its Japanese reading « エル » on the other and a bookmark ribbon in the gutter, replaces the raster « A 文 » logo (favicon, sidebar, login, loading screen, README), and the ribbon marks the current page of the sidebar, the login card and the passage being written. Colours still come only from the design tokens and keep WCAG AA contrast.
Fixed
- Changing licence no longer transfers another licence’s word usage. Monthly counters are now stored per signed licence identifier. Activating licence B on a machine that previously used licence A starts from B’s own server-side total; returning to A restores A’s local high-water mark. Existing installations assign their history to the certificate active during migration (#146).
[0.13.0] - 2026-09-23
Libris opens up to the rest of an organisation’s tooling. This release signs people in through an LDAP or Active Directory directory, imports sources from and sends finished books to a Nextcloud/WebDAV server, exports and restores a whole series in one archive, and speaks the reader’s language everywhere it used to fall back to French: mail, decision log, API reports, MCP answers and the documentation itself.
Added
- LDAP / Active Directory sign-in. An administrator configures a directory under Settings ›
Single sign-on (OpenLDAP and Active Directory presets,
ldaps://by default, StartTLS, custom CA, service account, search base and filter, optional allowed and administrator groups, optional account creation on first sign-in) and tests it before enabling it. The certificate and host name are always verified, filters are escaped, an empty password is refused before any bind (an anonymous bind “succeeds” on many directories), and unknown user, ambiguous entry and wrong password all get the same answer, counted by the same rate limiter as local sign-in. Accounts are matched byentryUUID/objectGUID, never by name or address; a local account is linked only by an administrator. Name, address and password belong to the directory; two-factor authentication stays Libris’s own. Setup guide:docs/ldap.md(#132). - Nextcloud / WebDAV connections. Each account can register its own WebDAV connections (My
account), browse them from the import wizard (“From Nextcloud/WebDAV”) and send a finished book
to a remote folder in any export format, with the same pending/sent/failed states and retries as
library publication. The password is encrypted with
SECRET_KEYand never returned. The feature is off until an administrator lists the allowed hosts inWEBDAV_HOSTS; HTTPS is required except on networks named inWEBDAV_PRIVATE_NETWORKS, the request goes to the address that was checked, redirections to another host are refused and reads are bounded byMAX_UPLOAD_MB. Google Drive, Dropbox, OneDrive and Mega are not covered yet (#125). - Export and import of a whole series. Export the series writes one ZIP holding the series (settings, style sheet, Series Bible and its validation, glossary, identities and merges, their links to the volumes’ entities, relations) and the project archive of every volume, archived ones included. Restoring it creates a new series (“Name (2)” when the name is taken), remaps every identifier and validates everything before writing anything; a faulty archive is refused with the field or volume at fault. Providers, members, suspension, the original audit log and OpenViking documents are deliberately not carried (#142).
- Special volumes. A volume that has no ordinary number — “DX1”, “EX”, “LN 14+”, an anniversary book — can carry a free label instead, typed in the same field as the number (a whole number is a number, any other text a label). A label that starts with a number is read right after that volume (“14+” after 14); the others come at the end of the series. Detection no longer invents a number it is not sure of: “14+” is no longer volume 14, and one odd file name no longer strips the numbers of a whole batch. Chapter numbering also reads “3-12” and “Title 12 - Subtitle” correctly (#139).
- Other accounts’ jobs in the queue, greyed. The queue now shows, greyed and without any link, the title, owner and state of the jobs of books one cannot open, so everybody can see what is occupying the shared providers. Nothing else of those books leaves the server: their pages still answer 404 (#130).
- Select every volume of a series at once from the volume list (#143).
- A short explanation under every book and series setting, checked against what the setting actually does (#127).
- Job controls are audited. Pausing, resuming, retrying or cancelling a job — one book, a series or a selection, from the interface, the API v1 or MCP — now writes an audit entry naming the account, the channel and the token used (never its secret), without any book content. A new Account log card under Settings › Users shows an account’s entries to administrators, and the series log names who controlled its jobs (#117).
Changed
- Uploads up to 256 MB.
MAX_UPLOAD_MBnow defaults to 256 (was 60) andMAX_UNPACKED_MBto 1024 (was 300); uploads without a session stay bounded to 1 MiB. An upload is still read in memory: count up to about 1.3 GB of RAM for the largest, image-heavy EPUB (#140). - The integrated reader opens over the book instead of replacing the whole application, and closes with Escape, its close button or a click outside; an unsaved correction asks before closing (#128).
- Book and series settings save once. On a book, the book, languages, strategy, style sheet and autopilot blocks share one Save settings; on a series, the defaults, the failing-model block and the style sheet share one Save. When one part is refused, what succeeded is kept and the refused part is named, with its draft left on screen. Budget, sharing, review links, another language, source watch, provider comparison and the danger zone keep their own action. The style sheet was a form nested inside the series settings form, which let Enter submit the wrong one (#126).
- “Next actions” of a series groups volumes that share the same issue into one line with their count and a series-wide action, instead of one line per volume (#123).
- API tokens live only in My account. Settings › Automation API keeps the installation’s webhooks and points to My account › API tokens (#122).
- Everything a person reads is in their language. Every mail (invitation, password reset, delivery, alerts, SMTP test), the autopilot decision log and stored errors, the API v1 completion report, import decisions and webhooks, MCP tool descriptions and answers, and the comments of the Word export are now written in the language of the person who reads them. A new test fails on any French sentence of the backend without an English version (#133).
- Documentation in French. The Documentation screen serves the French API reference and change
log when the interface is in French.
api,configuration,docker,backup,mcp,autopilot,openviking,codexandldapexist asdocs/*.fr.md;CHANGELOG.fr.mdcovers 0.8.0 onward. Developer documents stay in English (#129).
Fixed
- Administrator pages opened by a non-administrator now say that the page needs an administrator account instead of silently showing the library under a “Settings” title (#120).
- Book settings list the stronger model before the fallback chain, the order in which they are tried, as the series settings and the import wizard already did (#121).
- Library cards in grid view no longer let a long provider name push their content out of the card (#138).
Upgrade notes
- Three database migrations (WebDAV connections, volume labels, LDAP identities). Back up the database first, as for every release with migrations.
- An
.envthat pinsMAX_UPLOAD_MB=60keeps 60 MB: remove the line or raise it to benefit from the new default, and check that a reverse proxy in front of Libris accepts bodies of that size (for nginx,client_max_body_size 260m). - WebDAV and LDAP are off by default. WebDAV needs
WEBDAV_HOSTS(andWEBDAV_PRIVATE_NETWORKSfor a NAS on the local network); LDAP is configured and enabled from Settings › Single sign-on. Keep a local administrator account when enabling LDAP. - The licence protocol is unchanged since 0.12.0: no licence server update is required.
[0.12.0] - 2026-09-22
Added
- Model failures counted by cause, across the whole installation. Every failed request was
already categorized in code the moment it happened, but the category was thrown away; the only
place a failure was visible was one book’s own recovery tab.
RequestLognow keepserror_kind(9 fixed categories), a new admin endpointGET /api/statistics/failuresand a “Failures by cause” panel on the Statistics screen show provider, model, cause, count and share across every book, so a recurring problem with one provider or one kind of answer stands out instead of hiding book by book (#137). - A series-wide autopilot toggle. One button now starts or pauses the autopilot for every volume of a series at once, replacing “Translate everything” (a one-off launch) and the separate suspend/resume control, which described a different mechanism and said nothing about the autopilot. A series only counts as active once one of its volumes has actually had a job (#131).
- The autopilot audits series consistency, not just its own book. Once a volume settles its own convergence loop, the series-wide consistency audit (glossary, genders, identities, style sheet) now runs automatically if it belongs to one — no model call, only a comparison of what is already stored. The book’s autopilot tab shows a new “Consistency” badge with the count of divergences found and a link to the series’ own tab; nothing is corrected automatically, since each divergence needs a human choice of which contradicting value wins (#134).
Fixed
- Missing English translations for the panels 0.11 added. Twelve texts shipped in 0.11 had no
English version; an English interface silently fell back to French for them. The translation
guard (
e2e/translations.spec.ts) now refuses missing translations as well as collisions, not only collisions (#119). - A book’s active step is read from the server, not guessed. The interface computed a full, separate progress state whenever the server’s own progress was absent, a path that in normal use never triggers, but that could still silently disagree with the server for the same book. It is replaced by a plain rule: the first step whose own numbers do not say it is finished (#136).
- A series translates one volume at a time, in order. Launching a whole series used to start every volume together, so a provider outage on one volume never stopped the next one from starting, and several running volumes split the capacity of the same provider for no benefit. A volume’s translation now waits for every earlier, non-archived volume of the series to have settled its passages first; analysis can still overlap (#124).
- An escalation decision now tracks the setting it was made against, not just the job. Once a failing step had escalated to a stronger model, nothing re-read the “stronger model” setting for that step again until the job ended — so changing or clearing a broken setting did not free the step stuck on it, and the job could never finish. A decision now also remembers which setting it was made against and is re-evaluated whenever that setting has since changed (#141).
- A series’ settings now reach its existing volumes. Changing the source or target language, provider, quality, memory source or instructions on a series used to apply only to volumes imported afterward; every existing volume kept whatever it had, decided or not, forever. Saving the series now propagates each changed field to every volume that has not set it for itself. A volume marks a field as its own the moment somebody changes it directly on that volume, and the series never overwrites that field there again. A field the series leaves undecided never overwrites a volume that has a value (#135).
- A real contextual button for a series, “Resume” distinct from “Start”, and a separate Cancel action. The series-wide toggle added earlier in this release still had only two states (Start/Pause) where a suspended series needs three (Start/Pause/Resume, without silently lifting the suspension and launching in the same click) plus a separate, always-confirmed Cancel for jobs that are pending, paused or blocked, not only running (#118).
[0.11.0] - 2026-09-22
Libris now carries a whole series from translation to the reader’s library. This release adds provider batches, durable series-wide controls, translator notes, a full-page correction reader and watched-folder publication. It also turns local accounts and SMTP delivery into complete administrative workflows, with recent identity confirmation around sensitive account changes.
Added
-
Publication to a watched library folder. Queue a finished EPUB from its book screen or automatically after translation, with installation settings and visible pending/published/failed states. Failures retry independently from translation. Stable identifiers keep namesakes apart; a newer publication request fences an older in-flight file. Text-origin books are also converted to EPUB. Configure the external library’s scan/import separately; this is not an OPDS push API. Standard EPUB series metadata carries the actual series name and volume position without renaming stable file paths. Komga and Kavita indexing and reading are verified; a metadata update may reclassify a series inside the external library, so its bookmarks are not guaranteed.
-
Translator notes for readers. Accepted glossary terms carry a separate reader-facing note, placed at the first occurrence, every occurrence or nowhere. A dedicated model operation proposes notes without applying them; editors review each proposal. EPUB exports use linked footnotes, text/Markdown put notes at chapter end, and Word exports anchor comments. Retained originals receive no notes. JSON, CSV and TBX glossary exchanges preserve the settings.
-
Persistent suspension of a whole series. One server operation pauses, resumes or cancels its volumes with a per-volume report. Suspension also prevents source-watch autostarts; reading, correction and export remain available. Excess resumes wait durably for account/token/provider capacity. The interface,
/api/v1/series/{id}/jobs/{action}and MCPcontrol_seriesshare the rules. -
An integrated reader with inline correction. Read chapter by chapter, optionally display the source, then edit a paragraph without losing the reading position. Corrections use the existing revision-checked endpoint; a conflict retains the draft. Read-only members cannot edit, preserved originals are identified, and structured units and protected formatting remain intact.
-
Administration of local accounts. Administrators can create local accounts, update username, recovery email, password, role and activity, remove local authentication factors, and delete an account with explicit transfer of its library. SSO identity remains managed by its identity provider. Last-administrator and active-work safeguards prevent leaving an unusable installation.
-
SMTP settings and a delivery inspector. Configure mail in the interface, test through the actual worker queue, inspect pending/sent/failed messages and retry eligible failures. Passwords are write-only; the inspector does not reveal message bodies or password-reset links. Environment defaults remain available, and TLS certificates are verified.
-
Recent identity confirmation for sensitive account changes. A five-minute proof belongs only to the current session. Use the existing password and required second factor, a registered WebAuthn key with user verification, or fresh SSO authentication. A silent SSO session alone is insufficient. The confirmation preserves the form draft and retries the refused change once; it does not claim to protect every provider, SMTP or licence setting.
-
MCP glossary propagation.
propagate_termpreviews by default; writing additionally requires the JSON booleanconfirm: true. It returns counts and examples, versions every changed passage and preserves human protection. Together withcontrol_series, the catalogue now has 28 tools. -
Permanent provider retirement. Remove stored credentials and future availability while retaining request history. Account deletion handles personal providers and Codex connections explicitly; removing a stored API key does not revoke it at the external provider.
-
Batch calls to OpenAI and Anthropic, at half price. A provider can be set to send the calls of a job as a batch: both vendors charge half for what may wait, and a book translated overnight is exactly the work that can. The intention is written down before the paid POST, so a lost acknowledgement blocks and asks rather than paying twice; a submitted batch waits through
waiting/next_attemptwithout counting as an outage, and is collected again after a worker restart. Costs are recorded at the discounted price actually paid, budgets reserve the committed maximum, and a provider that answers 404 to the batch endpoint falls back to synchronous calls while saying that the announced discount no longer holds for them. Twenty-four hours is the vendors’ processing window, never a delivery promise: a pipeline whose steps depend on each other takes several successive batches, and a cancellation does not refund what is already processed. -
Translate a whole series in one request. Tout traduire, on the series screen, runs the whole pipeline — analysis, translation, review — on every volume. Importing a forty-volume saga and then starting it volume by volume was the ordinary way to use this product, and it should never have been. Each volume keeps its own guards and the report names them: no provider, a spent licence, a crossed budget or a full queue refuses that volume and not its neighbours; a volume already working carries on rather than being started twice; archived volumes are left out. A suspended series refuses the start and says so — nothing lifts a suspension quietly. The automation API (
POST /api/v1/series/{id}/jobs/start) and the MCP toolcontrol_seriesdo the same, and both ask forpipeline:starton top ofjobs:control: a token allowed to stop a series must not be able to bill one. -
The stronger model and the fallbacks, decided once for a whole series. A reader who follows a forty-volume serial had to say the same thing forty times: the model that carries a failing step and the ones that stand in for an outage existed on the volume and on the installation, with nothing in between. The series now sits between them, under Series settings › Quand un modèle échoue, and follows the rule the style sheet already follows — what the volume decides wins, what it leaves open follows the series, and what the series leaves open follows the installation. A provider named twice is tried once: a chain that waits out the same outage twice wastes an evening. Database migration.
Fixed
-
OpenViking cleanup recovery after a root change. Administrators see blocked counts across the full queue and can retire a pending cleanup without deleting remote data or its audit trail. An optional daily orphan scan stores its report across restarts and never queues removals; directory and request limits refuse an incomplete inventory instead of reporting a false clean state.
-
A passage that resists is carried to the stronger model before any stand-in, and an unfinished book no longer calls itself finished. Two different troubles wore the same answer: a model that cannot do this passage — an answer that never parses, a refusal, paragraphs lost on the way — was handed to a stand-in, which is the answer to an outage, not to a model that is simply not good enough. The recovery ladder now climbs to the stronger model (the volume’s, else its series’, else the installation’s) after everything the first model can be asked to try again itself, and only then to the fallback providers; a provider that is both is climbed once. And when passages still could not be translated, the autopilot’s badge says “Terminé · 3 passages non traduits” in orange rather than announcing a success in green: a book missing a passage is not a finished book, and a green badge is the reason nobody goes looking for it. The import form — every path of it — now offers the stronger model and the fallback chain, since that is where somebody decides how a book will be translated, hours before a failure is discovered.
-
Mail reservations survive worker restarts. Concurrent workers reserve a queued email before sending it, renew the reservation during slow SMTP and acknowledge only their own attempt. A new manual retry cannot be acknowledged by an old sender. Exhausted attempts remain visible; a crash after SMTP acceptance may still cause a duplicate, so delivery is not advertised as exactly once.
-
Safe restore before image rollback.
librisctl restorevalidates the archive, stops writers and takes a rescue dump before recreating only the application’s database. This avoids foreign-key failures from tables absent in an older backup.--no-startleaves writers stopped so the intended older image can be selected without immediately reapplying newer migrations. Schema-changing rollback remains a deliberate restore operation, not an automatic destructive downgrade. -
SSO authentication boundaries. Local passwords and security keys cannot bypass the external identity provider for an SSO account; local factor management is not offered for those identities.
-
Confidential database diagnostics. Job errors and logs do not copy SQL literals or driver messages that may contain manuscript excerpts. Even an exception with an empty message is handled.
-
Model answers carrying forbidden control characters. NUL bytes no longer poison stored translation or arbitration data. A persistent data refusal is reported as a failure instead of being retried forever as a temporary database outage; genuine connectivity failures still wait.
-
MCP input and spending boundaries. Strict confirmation and argument types, bounded request bodies and message batches, per-message throttling, token budget/queue attribution and uniform inaccessible-object refusals. Invalid Unicode is rejected before any tool writes.
-
Cross-volume permissions. Series-wide reports and propagation only include volumes the caller may respectively read or edit; possession of one shared volume grants no series-wide control.
-
Public review links. Bearer secrets no longer travel in API paths; reads are rate-limited and paginated, responses prohibit caching, and visit counting is grouped. Revocation blocks future requests but cannot erase text a reader already downloaded or copied.
-
Bounded editorial scans. Glossary propagation, repetition reports and correction learning reject oversized selections before loading their text. Thai and Lao terms work inside unspaced text. Exact casing is intentional; human revisions remain protected even when explicitly included.
-
Honest quality suggestions. Repetition candidates are not presented as proven literary flattening. Correction learning uses actually applied model versions, not rejected responses, retained originals or a person’s later revision of their own wording.
-
Word export safety and fidelity. Bound selected passages, text and diff work; long changes remain complete paragraph revisions. Rejected model responses no longer appear as prior drafts. Invalid XML characters produce a localized refusal. LibreOffice round trips preserve tracked changes and anchored comments; Unicode chapter downloads are verified in Chromium and Firefox.
-
Work windows and accounting. Real elapsed instants handle repeated or missing daylight-saving hours; daily spending uses the installation’s accounting currency. Resume rechecks licence and budget guards even after a manual pause. Account metering avoids PostgreSQL foreign-key deadlocks.
-
Licence usage under concurrency. Installation keys and report sequence numbers are allocated atomically; paid passage usage and installation totals are reconciled without counting a restart twice or losing words during concurrent work.
-
Documentation contracts. MCP counts follow the running catalogue, Codex configuration uses TOML rather than opencode JSON, and privacy/cost guidance identifies configured external recipients and already admitted calls instead of promising impossible isolation or prepaid spending caps. In-app paragraph links stay on the documentation screen, and long contents lists no longer push the changelog below several screens on a phone.
Upgrade notes
- Back up database, book files and the original configuration/secrets as one recoverable installation.
This release’s migrations add provider retirement (
8c4f2a9d6b31) and series suspension (7a1e8b3c5d90), reader notes (9d7c4a2e6f18) and the publication queue (b2e6d1f8a490). Mail ownership addsc8e4f1a9d270, then session confirmation addsd3a7c9e1f650. Series-wide stronger-model and fallback settings finish the chain ate5b2d8c3f710. Stop API and workers before upgrading. Existing sessions do not gain a fresh identity proof; a downgrade removes pending SSO confirmations but preserves ordinary login requests. - Before tagging, compare
backend/app/licence/server.pywith the last release and verify that the deployed licence server accepts and verifies the client’s signed requests. Its repository’smainis not evidence of what runs on the server. - Image rollback and database restore are different operations. A schema change requires the documented restore procedure and an isolated rehearsal, not an assumed automatic downgrade.
[0.10.0] - 2026-09-21
Libris opens up, and remembers what you decided. This release is about the two things a long translation loses: the decisions already made, and the people who are not you. A coding agent — opencode, Claude Code, Codex — can now drive the whole installation over MCP; a glossary decision taken at chapter 300 catches the 299 chapters behind it in a second and for nothing; what you keep correcting is offered back as a rule instead of dying with each passage; and the turns of phrase a model reaches for again and again are finally measured rather than felt.
For the people who are not you: a review link opens one book to a proofreader who has no account here and takes no seat of your licence, and a Word export carries your corrections as tracked revisions and the model’s doubts as anchored comments — the format an editor actually works in.
Before upgrading: one database migration (
review_links). Nothing about how anybody signs in changes, no setting is required, and nothing new costs money on its own. The work window and the daily ceiling are off until an administrator turns them on.
Added
-
A Model Context Protocol server (
POST /mcp). opencode, Claude Code, Codex — anything that speaks the protocol — can read the library, set a book up, keep the glossary and the series, start and steer the work, and read the translation back. Twenty-six tools, and every one of them calls the handler the web interface calls:access()still decides which book is yours, the licence guard still refuses work that costs words, budgets still pause a job that overspends, and the audit log still records who did what. A second set of rules would have been the set that is wrong.Authentication is the automation token and nothing new — same
lbr_…secret, same rate limit, same account. Three scopes are added so an agent can be given the narrowest token that does its errand:library:read,library:write,library:delete. A token that reads a library cannot empty it, however it is asked to, and the refusal names the missing permission. Anything that deletes needsconfirm: trueand, without it, answers what would have gone. Lists are paginated and long answers cut, with the total always stated: a library of five hundred books is not an answer, it is a context window. See docs/mcp.md. -
A glossary decision reaches what is already translated. Deciding at chapter 300 that 金丹 is a Noyau d’Or and not a Pilule Dorée left the 299 chapters before it wrong: the glossary governs what comes next, never what is already written. Changing the translation of a term now answers how much of the series still carries the old word, and offers to replace it there. No model is called — it costs nothing and takes a second, where retranslating the volumes would be paid twice.
The word is replaced only where it is the word, and a script that does not space its words (Chinese, Japanese, Korean) is replaced literally. A passage corrected by hand is left alone, counted apart, with a second button to include it explicitly; nothing is written before the number of passages, occurrences and chapters is shown, with a sentence before and after. Every passage keeps its version, so the whole thing reads and undoes itself like any other correction.
-
What you keep correcting, offered back as a rule. Corrections already win over anything the model produces — but only in the passage they were made in, so the same decision was made again in volume 2 and again in volume 7. The substitutions that recur are now read out of the work already done and proposed, most settled first, with the number of times and the passages. Only corrections of machine output count: revising one’s own earlier correction is a second thought, not a rule. Nothing is applied — turning one into a glossary entry stays a person’s decision.
-
Translation mannerisms, measured. The signature failure of translation by language model is the one nothing here measured: passage by passage the work looks fine, and read end to end the same hand shows everywhere. What is measured is not that a phrase recurs — a novel repeats itself on purpose — but how many different source passages one translated phrase stands for. When one French turn of phrase covers thirty different English ones, the author wrote thirty things and the translation says one. Three families: flattening (start here), frequent turns of phrase (refrain or crutch, a person decides), and vocabulary richness read against the book’s own average. No model is called, and
series=truereads every volume at once, where a habit shows best. -
A review link, for somebody with no account and no seat. What a licence sells is accounts on one installation, so showing a translation to a proofreader or an editor cost one of them — or meant sending files back and forth, which is the best way to lose a correction. A link opens one book, in reading only, expires, and is revoked in one click. No account is created by following one, no session is opened, no cookie is set; only the hash of the secret is kept, so the address is shown once and cannot be read back. The original does not travel unless the link was made to carry it. Every reason a link fails answers the same 404: whoever holds an address learns nothing about whether it ever meant anything.
-
A Word export, with the corrections in track changes and the doubts in comments. DOCX was accepted at the import and did not exist at the export, which left the one format a professional editor works in out of reach. What the model wrote and what a person left are compared word by word and become real revisions, which an editor accepts or rejects as they would any translator’s work; an original kept, a doubt the model raised, a review note still open or a low score becomes a comment anchored on the passage rather than a report nobody opens. Uncheck the revisions to hand over a clean document — what one sends a client rather than an editor.
-
A work window, and a ceiling for the day. A fair queue knows how to order work; it did not know how to wait. Models are cheaper and less busy at night, and a book budget stops one book while nothing stopped an installation from spending a month’s worth in an afternoon. Both live in Settings › Budgets and are off by default. Neither interrupts anything brutally: outside the window, or over the ceiling, a job in flight is deferred, not cancelled — it goes back to waiting with the next opening as its next attempt, so it starts again on its own with nothing to click, and its message says when. A window that crosses midnight is the ordinary case.
Fixed
-
A misunderstanding is no longer read as a revocation. Renewing a certificate erased it on any 4xx the licence server answered. A 422 means “I did not understand your request” — it says nothing about the licence — and it cost an installation the three days of grace that exist precisely so that trouble at the licence server never interrupts a reader. That is what stopped production on 2026-09-21: 0.9 began sending a field the deployed server did not know, and Libris read the refusal as a revocation while the licence was valid the whole time. Four codes now take the certificate away, and they are the four that mean the licence is no longer valid; everything else is treated as the outage it amounts to.
-
One message that could not be sent no longer stops every mail behind it. A book title carrying a line break — typed, or read from the
dc:titleof an imported EPUB — cannot be written into a mail header. That refusal is correct; it happened inside the outbox loop, after the message was queued, and it was caught by nobody: the dispatcher died, the rollback meant the attempt count never moved, and the same message was at the head of the queue after every restart. Everything behind it stopped for good — a finished book, a quota running out, a licence that could not be renewed, and the reset of a password. The break is now removed where the header is built, and anything that cannot be sent is set aside and named rather than left in the way. -
Exporting a chapter whose title is not latin-1 no longer answers 500. A header goes on the wire as latin-1, and the file name was written into it raw: a chapter called 第一章 or Глава 1 — the ordinary case for this product — raised an encoding error on the way out. It now uses the encoded form the automation API has always used, and the name survives rather than being dropped.
-
A licence that changes machine keeps the seats it reports. The branch that moves a licence onto a new machine created the activation without the account count the instance had just sent, so the back-office showed an unknown count and the seat anomaly went unrecorded until the next heartbeat.
[0.9.0] - 2026-09-21
A Libris several people share. A licence sells an installation a number of words a month; this release says how the people behind it share that, and who they are. An administrator gives each account its share of the month and administers the providers of the team; every account sets up its own providers with its own key; an installation declares the currency it counts in, and a price list in another one is converted where it is typed. Signing in gains everything an account may want on top of a password: a code from an authenticator, a security key, or the organisation’s own single sign-on, each with the ten recovery codes that make losing a telephone survivable.
For readers, a webnovel becomes a book — Libris writes the EPUB itself for a volume that never had one — and a range of chapters may be asked of it, named after the chapters inside. The reference of the API and this changelog are read inside Libris, so a client reads the documentation of the version they run. And a production incident is fixed at its root: a connection pool sized for the providers, and a failure that stops calling itself a database that was never unavailable.
Before upgrading: six database migrations, and the supplied Compose file now sets
max_connections=200on PostgreSQL. Recreate the containers (librisctl up) rather than restarting them — a restart keeps the database the old file created. Nothing about how anybody signs in changes until an account turns a second step on, or an administrator configures a provider.
Fixed
-
Observability stops reading like a job that has died. An analysis spends long stretches writing to the database without one request to a model:
consolidationwalks the passages to rebuild the timeline,memorywrites analyses already computed. For an hour at a time the panel showed 0 requests in flight and a table whose last line was an hour old — which reads as something broken, while the book was advancing normally. The panel now names the step under way and says, in as many words, that it makes no call to the model and that no request is expected while it lasts.In the same card, “1 h 40 left · high confidence” was drawn from the throughput of requests during a step that makes none: it was measuring something that was not happening. The estimate now answers
silentfor those steps, and the interface shows a dash rather than a number it cannot stand behind. What has already been spent is a fact, not an extrapolation, and stays.And every measure the endpoint answers is now named:
input_tokens,cache_hits,duration,costandactiveused to be drawn as they are written in the database, beside labelled ones. Seconds are written as a length of time and money as money. -
Two panels can no longer disagree about the same word. The interface’s translations are one table keyed by the French text, so a generic key registered in two places —
Activer,Faible— took whichever meaning the import order happened to give it, and the English shown changed from one build to the next. A test now refuses any new French key registered with two different English values, and lists the ones already there so the list can only shrink. -
A connection pool sized for the providers, and a failure that says its own name. A production installation logged 113
database_unavailablein five hours while PostgreSQL was perfectly healthy — clean logs, regular checkpoints, not one lock waiting. The engine had never been given a pool size, so it used SQLAlchemy’s default of 5 connections plus 10 in a burst; a provider with a capacity of 16 alone could empty it, and every caller then waited five seconds and was answered with aTimeoutError, which is the same exception class as a database that is down. Nothing was lost — the lease grace put every job back — but the morning went into reading the logs of a database that had nothing wrong with it.The pool is now a setting (
DB_POOL_SIZE,DB_POOL_MAX_OVERFLOW,DB_POOL_TIMEOUT), because the demand changes without a redeployment: an administrator raises a provider’s Concurrent books from the interface. Its default covers, with room to spare, the sum of the providers’ capacities — not the largest of them, since books running on different providers add up — plus the loops that never stop. Both processes now say at start-up whether it covers them:db_pool=ok capacity=… providers=…, ordb_pool=undersizednaming what to raise, so the next person to reconfigure a provider reads one line instead of repeating the diagnosis. The supplied PostgreSQL getsmax_connections=200in the same move: raising a pool without raising what is behind it only moves the failure. And a pool that ran out is logged aspool_exhaustedrather than as a database that was never unavailable. -
Changing the address of an account no longer fails on a field the server itself wrote. My account › Notifications answers what it holds and what the installation can do — whether a mail server is configured, whether a delivery folder exists — and the form sent that whole object back, as a form does. The endpoint refused it over those two read-only fields, with a 422 nobody could act on:
configured: Extra inputs are not permitted. It reads them and ignores them now, and the interface only sends what it may change.
Changed
-
The Observability tab always says which step, and how far into it. The counter was in the header of the book and nowhere else; the panel that exists to answer “where is this?” only named a step when that step happened to make no model call. It now shows, for every running job: the step, how far into it (
421 of 1071), and — when the order of the steps is known ahead of time, which is the case for an analysis — which of how many (step 4 of 5). A Book Bible built as a tree adds its level, the autopilot adds its phase, and a book with nothing running says so rather than showing an empty card.A translation deliberately claims no position: its own sequence depends on what its launch asked for — a recovery pass, a consistency pass, a final review — so announcing “step 2 of 5” out of a sequence that may hold three or six would be one more number that does not hold.
-
Counters that say the same thing twice, and a status that names its stage. A series card read
100 % traduit · 0/986 validésnext to a pill sayingEn cours: three numbers that contradicted each other, because the second one counted human validations — a measure of reading, not of progress, legitimately zero for a book the autopilot translated. The card now says how many passages there are, how much is translated, and then what is in the way: passages failed, passages to review, or ready to export when there is nothing. The pill names the stage that is actually running — Analyse en cours, Traduction en cours, Relecture en cours — rather than a generic En cours. -
Languages are chosen from a list, by name and code, in the book’s settings, the series’ defaults and the import assistant; a field stays for a code Libris does not know by name. The stored value is still a code, so nothing changes for the books already in the library.
-
The Series Bible reads as blocks rather than as a wall. Universe, places, conventions, chronology and characters each sit in their own frame, the rows of a list are told apart, and a character card is sunken inside its section.
-
A licence sells accounts, not machines. One licence is one installation; what it allows is the number of people who may sign into it. Libris now reports how many active accounts it carries, refuses the creation of one past what the licence allows — and only that: signing in, working, reading and exporting are never touched, because a licence that shrinks must not lock anyone out of the installation they are working in. Turning an account back on takes a seat back and follows the same rule; turning one off is always allowed. Settings › Comptes says how many seats are held of how many. Machines are still counted and shown, with a wide ceiling.
-
The month’s words reach the licence server when a job ends, not only at the next heartbeat six hours later: one call per job, never per passage, and an unreachable server costs nothing since the next heartbeat carries the same number.
Added
-
Single sign-on (OpenID Connect), for the installations that belong to a team. A company already has a place where people are created and removed; asking it, rather than keeping a second list of passwords beside it, is what makes leaving actually close the door. An administrator gives an issuer, a client id and a secret (Réglages › Authentification unique) and Libris does the rest: it discovers the provider rather than being told about it, sends people with a
state, anonceand a PKCE challenge, and verifies the identity token it gets back — signature against the provider’s own keys, issuer, audience, expiry, and the nonce that ties the token to the request. A sign-in that is missing any of those is refused and says which.An account is found by what the provider calls it (
sub), never by an address it happens to send: an address changes hands, and matching on one would hand an account to whoever inherits a mailbox. An unknown person is refused until an administrator makes their account, unless the installation asks for accounts to be created on first sign-in — in which case each new person takes a seat of the licence, first come first served, refused like any other.Two things follow, and Libris says them rather than leaving people to find out. An account opened by a provider has no Libris password: Mot de passe oublié tells it so instead of promising a mail that will never come, and no password signs it in. And the second step is the provider’s: an organisation that requires a key or a code at its own door has already had it, so Libris does not ask again. Verification is
pyjwt’s — algorithm confusion and key selection are exactly where a hand-written check goes wrong. Database migration. -
Security keys, for the accounts that want one. A code from an authenticator answers a shoulder and a leak; it does not answer the third thing — a page that looks exactly like Libris and asks for the code as Libris would. A security key does, and by construction: it signs a challenge this server drew, for this domain, and a page on another domain cannot ask it for a signature Libris would accept. Mon compte › Double authentification registers one, names it, and shows when it was last used; a browser that cannot do it — no HTTPS, an older browser — is told, rather than shown a button that cannot work.
A key is one more way through the second step, beside the authenticator’s code and the ten recovery codes, not a replacement for the password. An account may hold a key, an authenticator, or both; holding either is what puts the second step in force, and the first one registered brings the recovery codes with it. Removing the last key of an account that has no authenticator ends the second step and the codes with it.
Two rules are said out loud rather than folded into “invalid”: a key whose signature counter does not advance is a copy of a key, and is named as one; and a key is bound to one domain (
WEBAUTHN_RP_ID, else the host of the first allowed origin), so an installation reached at two addresses registers under one of them. Verification is thewebauthnlibrary’s — the one thing in this product that must not be written by hand. Database migration. -
The reference and the changelog, read inside Libris. “Where is the API documented?” had one answer — a file in a repository a client has no access to — and “what changed in this version?” had none at all. Both documents now travel inside the image and are read from the application (Documentation, in the navigation, for every account): the reference somebody reads is the reference of the version they are running, not of whatever is on a website today. The API tab carries the whole reference with its examples and its tables, with a table of contents beside it; the Nouveautés tab carries this changelog. The OpenAPI schema is still there for the tools that want one, a link away.
Markdown is drawn as elements, never as HTML: setting HTML into the page would mean trusting a document to contain no script, forever, in every version. A small renderer written for this reads what the documentation uses — headings, paragraphs, code blocks, lists, tables, quotations, and inline code, bold, italic and links — and shows anything else as the text it is.
-
A second step when signing in, for whoever wants one. A password is what most break-ins use: read over a shoulder, taken from another site’s leak, or typed into a page that only looks like Libris. An account may now ask for a six-digit code from an authenticator application on top of it (Mon compte › Double authentification): a QR code to scan, the code typed back to prove the application works — a second step nobody can pass is a locked door with the key inside — and only then is it in force. Signing in becomes two moments: the password answers half a sign-in, which opens nothing and lasts five minutes, and the code finishes it. A code is refused for the rest of its own thirty seconds, so one read over a shoulder is worth nothing to whoever read it.
Ten recovery codes come with it, shown once and kept hashed, each good for a single sign-in: a telephone is lost, dropped, replaced, and a security measure that can lock somebody out of the books they have been translating for months is a measure nobody turns on. They can be renewed at any time, which retires the old ones.
An administrator may take it off an account, never put it on (Réglages › Utilisateurs): a colleague locked out is a problem an installation has to be able to solve, while choosing somebody else’s authenticator is not an administrator’s call. Every removal is written in the audit trail with who did it, and ends that account’s sessions. The automation API is untouched — a token is a secret of its own, and carrying a code in an automated call would mean keeping the secret next to it. TOTP is implemented against RFC 6238 in the code rather than taken from a library; the QR code is drawn by
segno. Database migration. -
A webnovel becomes a book, and only the chapters somebody asked for. A serial lives in Libris as a text volume: TXT or JSON chapters, no EPUB behind them. Its reader could take away a ZIP of text files or one long consolidated file, and neither is something an e-reader opens — the EPUB export was refused outright for those volumes. Libris now writes the book itself: one page per chapter, a navigation document, the volume’s language and author, validated by EPUBCheck like every EPUB it produces. And a range of chapters may be asked of it — Options d’export › Du chapitre / Au chapitre, the one that already limited the text formats — so someone who has followed a serial to chapter 520 can take the last fifty and nothing else. The file is named after the chapters inside it (Le Rejeton 471-520.epub), because several slices of one book end up on the same shelf. An EPUB volume asked for a range gets the same reading copy, and the dialog says so: text without the original’s images and styling.
-
Every amount now says what it is. Provider prices are typed by hand, per million tokens, and nothing said in what: the interface showed bare numbers under a note reading “in the currency they were entered in”. An installation now declares one accounting currency (Réglages › Budgets): every price is in it, every cost computed from those prices is in it, and every amount the interface draws — estimates, budgets, statistics, provider comparisons — carries its symbol. One currency and not one per provider on purpose: a total that adds dollars to euros is not a total. Changing it states what the stored numbers are; it never rewrites them. On top of that, conversion where somebody types: a price list in dollars can be entered in dollars against an installation that counts in euros, converted once, on saving, at the European Central Bank’s daily reference rates — public, free, no account — fetched once a day by the worker and kept with the date they carry. An installation with no outbound access simply offers its own currency, and nothing else changes.
-
An installation shared by several people, administered as one. A licence sells an installation a number of words a month; until now one person’s three-volume series could eat the whole of it and nobody found out until the month stopped. An administrator may now give each account a share of the month — Réglages › File d’attente, next to the quotas that were already there, empty meaning the installation’s value and
0no limit of its own. The words are the same words the licence counts: source words, once per passage, charged to the account that owns the book, whoever pressed the button. What it refuses is what the licence refuses — a launch and a model call, never reading, exporting, correcting or deleting — and the book under way is finished inside the same overrun rather than left in half. Everyone sees what is left of their own month on the queue page; an administrator sees, account by account, what the installation’s month went on. Database migration. -
Providers that belong to someone. They were all the installation’s, set up by an administrator and offered to everybody. There are two arrangements now, side by side. An administrator sets up the installation’s own and may keep one unshared — the expensive model stays out of everyone else’s reach. And every account sets up its own, with its own key (Mon compte › Mes providers): a member’s provider is theirs, and no other account sees it, uses it, or could point a book at it. An installation that would rather hold every key closes the second arrangement (Réglages › Providers LLM), and only administrators configure anything from then on — what a member already put in place stays usable, they simply cannot change it. What was there before is unchanged: every existing provider belongs to the installation and stays shared. Database migration.
-
A way back into an account, a face on it, and a way out of it. Mot de passe oublié ? sends a one-hour, one-use link to the address of the account and answers exactly the same thing whether the account exists or not — a different answer would list the accounts of an installation. Using it closes every session of that account, because a password that was forgotten may also have been seen; only the hash of the token is kept, as for a session or an API token. An account may carry a picture (PNG, JPEG or WebP, 512 KB, recognised by its first bytes rather than by what the upload claims) and may be removed — its tokens and sessions with it, its books handed to somebody first, the password asked again, and never the last active administrator. An API token, finally, may be deleted and not merely revoked: revoked first, always, because a token that leaves the screen while it still works is a key nobody can close. Database migration.
-
A volume may follow the feed its chapters are published on. A webnovel comes out one chapter at a time, for months. Libris knew how to translate a new chapter as soon as it was given one — somebody still had to notice it existed, copy it and send it, and that waiting was the work. Suivre une source puts a feed address on a text volume: Libris looks at it every few hours, in reading order (a feed publishes newest first, a book is read the other way round), imports what is new and carries its text through the same path as any TXT chapter, and — if asked — starts the translation. Three rules hold it: a feed, never a page, because a feed is a published interface meant to be polled and scraping a site is not; an allow-list (
SOURCE_WATCH_HOSTS), with the same refusal of private addresses as an outgoing webhook; and what is imported is what the feed gives — an entry carrying only a link is remembered as seen and named to the reader, never fetched behind their back. A feed that fails is written next to the setting, and the watch goes on. Database migration. -
Comparing providers, from the interface. Libris translates with the model its reader chooses, and that choice is the one nobody can make for them: a model at a twentieth of the price may be enough for a light novel and hopeless on a book with three registers and a hundred names. The only way to find out was to translate a whole book and read it. Comparer des providers takes two to four of them, a handful of passages spread over the narrative chapters, and sends each passage through every provider with the same prompt and the same context, the answer cache off. The report gives, per provider: passages translated and failed with the reason, the average time, the real cost at the prices recorded with each call, the cost per thousand words, what the automatic checks found, and the quality score Libris would have given those passages, computed by the same function the dashboard uses. Adopter pour ce livre moves the book onto the chosen one. Nothing is written to the book — the translations stay in the report, side by side with the source — and the calls are real, paid, recorded under their own operation, and counted against the licence like any other model call. The comparison that existed as a maintenance script is now a job like the others: queue, licence, budget and a report kept with it.
-
The finished book, where the reader actually reads. Libris translates on a server and people read on a device; between the two there was a browser, a download and a cable, for a book someone asked for hours earlier and had stopped watching. A finished book now goes, if its reader asked for it, to an address of their own choosing — My account › Notifications, an address kept apart from the notifications’, because a Kindle address is not a mailbox anyone reads — and into a folder of the installation’s choosing (
DELIVERY_DIR), where Calibre-Web, KOReader or a sync client can pick it up. Neither is on by default, and setting the address is the only opt-in there is. The file is built when the message leaves rather than when it is queued, so the outbox stays small and a message that waited an hour carries the book as it is at that hour; a book aboveDELIVERY_MAX_MBis announced with its address instead of attached, because a mail server that refuses the attachment delivers nothing at all. A book that is not an EPUB, or not finished, is not delivered and nothing fails for it. Database migration. -
Terminology taken from a translation somebody already made. A reader who picks up a series where its publisher stopped has three volumes in their language already, and no way to tell Libris what the Tide Keeper is called in them: they would have to type the glossary by hand, term by term, and would miss exactly the ones they never thought of. Reprendre une traduction existante, in the glossary tab, takes a TMX — already aligned, which is what a professional hands over — or the original book and its published translation as two EPUBs, aligned here paragraph by paragraph on their lengths: a translated paragraph is about as long as its source, and the rare merges and splits show up as the cheapest path through the pair. From the aligned pairs it takes the names — a run of capitalised words that comes back on one side, and what comes back with it on the other — and proposes them to the glossary. A capital at the start of a sentence is not evidence, a name has to come back at least three times, and both
le Gardienandle Gardien des maréesare proposed so that the evidence decides between them. The proposals arrive unaccepted and unlocked, through the same preview, strategies and screen as any imported glossary file: an alignment is evidence, not a decision. -
The same book in another language, without importing or analysing it again. A book is imported once, read once and analysed once, and all of that is about the source: the chapters, the passages, who the characters are, how they are related, which of them a reader settled the gender of. Only the translation belongs to a target language. Getting a Spanish version meant importing the file again and paying the analysis a second time — and the two translations then disagreed about the names, because nothing tied them together. Traduire dans une autre langue creates a second book from the same source, carrying the chapters and their map, the passages, the characters with their aliases and the decisions made about them, the relations, the Book Bible, the analysis memories still attached to their own passages, the style sheet and the source file itself. What never travels is what is written in the first target language: the translations, and the glossary, whose translations are that language and nothing else. The summaries the analysis wrote stay in the first language — they are context for a model, not text for a reader — and a clone without the analysis is one checkbox away. The new book starts outside any series, because a series has one target language.
-
An audit of a whole series. Seven volumes translated over a year are seven occasions to call the same sword by two names, to give a character a gender book 1 never gave them, or to set the dialogue with em dashes in one volume and quotation marks in the next. Nothing is wrong inside any one volume — which is exactly why nothing caught it: every check Libris ran read one book at a time. The Cohérence tab of a series reads them together and reports a term translated two ways, a locked series term a translated passage does not carry (with the passage), a character whose gender or name changes from one volume to the next, and two volumes deciding the same style field differently where the series decides nothing. It costs no model call: everything compared is already stored. Apply to the whole series answers once — the chosen translation becomes the series’ locked decision and the volumes follow, except those that locked their own or deliberately depart from it, which are counted rather than overwritten; a gender is decided on every linked sheet; a style field moves to the series and the volumes give it back. A character named differently is shown but never merged automatically: that is a decision, and it belongs in Identités. A very long series is read up to a bound, and the report says so rather than pretending to have seen everything.
-
What a book will cost, before importing it. The last step of the import assistant now says what the files hold — words, passages, chapters — what they would cost with each configured provider, and what share of the month’s licence quota they would take. Nothing is created and no model is called: the numbers come from the inspection already done and from the owner’s own history with that provider when there is enough of it, the same arithmetic the estimate before a paid operation has always used. The words are counted the way the licence meters them, so a Japanese book is not announced at a third of its price. A book that does not fit in what is left of the quota is called out before the import rather than in the middle of the translation. The global consistency check is left out and said to be left out: it counts terms and characters the analysis has not discovered yet, and a made-up number would be worse than a named absence.
-
The gender of a character, decided once and agreed everywhere. French marks gender on participles, adjectives and pronouns, so a character whose gender is wrong is wrong in every sentence that names them — and until now the only way to fix it was to save the whole sheet, which freezes it: a validated sheet stops learning, and the analysis no longer adds a relation, an alias or a line of description to it. Gender and pronouns can now be decided field by field: those two become the reader’s, the rest of the sheet goes on. The gender is a value rather than a sentence — masculin, féminin, non binaire, indéterminé — and whatever the analysis answers is brought back to one of the four, so that two volumes of a series can be compared at all. A decided field is named as such in the prompt and carries the authority of a human decision: the translation agrees with it whatever the source’s own pronouns suggest, and the review reports a passage that contradicts it instead of keeping it. Deciding sends the translated passages that name the character back to review, as a glossary decision already did. Indéterminé is a decision too: a narrator who hides a character’s gender on purpose is a fact of the book, and the translation has to keep that door open.
-
The style sheet of a book, and of its series. Register, narrative tense, forms of address, honorifics and the typography of dialogue are decided once for a whole book, and they are exactly what a reader notices when a machine wavers: a
vousthat becomestubetween two chapters, quotation marks that change shape at the third volume. Saying it used to mean a paragraph of free text in the instructions — read as prose, followed or not, impossible to compare between two volumes and impossible to check. It is a handful of values now, under the book’s settings and under the series’: the series decides, a volume overrides it field by field, and what stays empty is left to the model, exactly as before. A couple of characters may depart from the general rule — Mara and the Keeper saytuwhile everyone else saysvous. The sheet reaches the model as a user rule, which the prompts already treat as the highest authority there is, and the review reports what departs from it. The typography of dialogue, the one decision a machine can verify on its own, is checked in the translated text: a straight quote where the sheet asks for« »raises a review alert — unless the source itself uses one, because a book is allowed to quote. Changing a sheet sends the translated passages nobody has validated back to review. Database migration.
[0.8.0] - 2026-09-20
Libris becomes licensed software. An installation activates once with the key its reader bought, then runs on a certificate the licence server signs and renews. This release also adds mail — a book finished, a job waiting —, a step that keeps failing being carried on by a stronger model, the author a series is by, and a configuration check that was looking at the wrong thing. Libris is proprietary software. Database migration.
Before upgrading: this version refuses the work that costs words without a valid licence. Activate yours in Settings › Licence right after the deployment. Everything already produced stays readable, exportable and deletable in every case: a reader locked out never loses a book. The image comes from the registry the licence gives access to: an installation must be told that registry once (see Changed).
Added
- A licence, and what it allows. An installation activates with the key its reader bought, then
renews on its own an Ed25519 certificate — licence, plan, fingerprint, quota and what is spent of
it — signed by the server and verified here against a public key that is a constant of the source.
The certificate is worth three days, which is also the grace an installation keeps when the server
cannot be reached: an outage never interrupts a reader, a revocation stops them at the first
renewal that gets through. It names the machine it was issued for, so copying it gains nothing. The
fingerprint has two halves: the host’s
machine-id, which changes with the machine and survives a redeployment, and an identifier drawn at the first start, which follows a restored backup — their disagreement is how a licence running twice shows itself. Compose mounts/etc/machine-idread-only, without which every recreation would look like a move. What is refused without a valid licence is the work that costs words: a launch, and every model call of a running job, which then pauses withlicenceas its reason. Reading, exporting, correcting by hand and deleting are untouched. The quota counts words of the source, once per passage, and a run of CJK characters counts as one word per two — otherwise a Japanese book would pass almost free and its French translation be charged in full. When the quota is reached mid-book, the book being translated is finished withinLICENCE_QUOTA_OVERRUN_WORDS, but no new job starts. Settings › Licence shows the plan, the shape of the key — never the key —, the words of the month against the quota, how long the certificate holds and the last contact; it is where one activates, renews by hand, and releases the machine before moving. Changing licence is one step: typing the new key is enough, the old licence gives its place back on its own so it stays usable elsewhere without waiting out the move cooldown, and the new key is granted before the old one is let go — a mistyped key does not cost the licence that was working. See configuration. - Libris writes when a book is finished, and when a job is waiting. A translation takes hours and nobody sits in front of it: there was no way to learn that a book had finished — or, worse, that a job had stopped two hours earlier waiting for a decision nobody knew it needed. Four messages, and a rule: one is sent when something ended or is about to stop, never when something merely began. A book finished; a job stopped and waiting (budget, unusable analysis, licence — not an outage, which retries on its own); the month’s quota at eighty per cent, once; and a certificate nobody has managed to renew for a day, the case one never notices because the installation goes on working until it does not. Silence is the default: not a setting to be turned off, but the absence of an address. An installation upgrading to this version starts silent; a reader gives an address in My account › Notifications, turns off what they do not want, and chooses French or English. Nothing is sent from inside a request: the message is written in the same transaction as the thing it announces, and the worker drains the queue with retries that grow. See configuration.
- One chapter’s translation, on its own. A volume followed over months holds hundreds of chapters, and downloading the whole book to reread the one just finished makes no sense. The editor’s chapter menu now downloads that chapter alone, as text or as Markdown, in a file named after it rather than after the book. Only a chapter that is actually translated: handing back one that is not would put the original where the translation should be, so Libris refuses and says how many passages are still missing.
- A step that keeps failing is carried on by a stronger model.
AUTOPILOT_ESCALATION_PROVIDERnames the provider that takes over when a step failsAUTOPILOT_ESCALATE_AFTERtimes out of the lastAUTOPILOT_ESCALATE_WINDOW. Only that step changes provider: the ones that follow stay on the book’s own. Unlike the fallback chain, this is not about a provider that is down but about one that cannot do that particular step. - The author a series is by.
series.authorsstays what the volumes say, rebuilt at every analysis;series.authoris what the reader decided, and it names the volumes that carry no author of their own.
Changed
- Libris is proprietary software.
LICENSEcarries a proprietary notice, in French and in English, and it says what a reader actually needs: what you translate is yours, and none of it ever leaves your server — the licence server sees a machine fingerprint and a number of words; and without a valid licence Libris stops translating but keeps giving back what it has already produced, so no book is ever lost. The image labels, the three package manifests and the OpenAPI document declareLicenseRef-Proprietary, andscripts/check_version.pyfails the build if one of them stops saying so. - EbookLib leaves the image. It is AGPL, it was a runtime dependency, and it therefore travelled
inside a proprietary image. Nothing in
app/ever imported it: it built the EPUBs the tests read, and one fixture that ships inside the image (scripts/check_epubcheck.py), now twelve lines ofzipfileand XML checked against the real EPUBCheck — the valid book still passes, the broken one still tripsRSC-007. EbookLib moves to thetestextra, andsixleaves with it. - The image comes from the registry your licence gives you access to.
scripts/install-docker.shtakes its registry fromLIBRIS_REGISTRYor from the.envan earlier install wrote, keeps it when it moves version, and says where to write when it has nothing to install. An existing installation must be told its registry once: put the address inLIBRIS_IMAGEin.env, or passLIBRIS_REGISTRYto the installer. Buying a licence gets you three things — the key, the address of the registry, and the credentials to pull from it.
Fixed
librisctl doctorcompared the wrong thing. It looked at a container’s age rather than at what it carries: a database container older than the last edit of the.envwas reported as stale although it never carried the variable that changed. Every variable of the file is now compared with what the container actually received, and the report names the service without ever printing a value.
[0.7.5] - 2026-09-20
A release for books taken from webnovel sites: what those sites write above each chapter is no longer read as the work, and a passage a provider refused can find its way into the Book Bible after all. No database migration.
Added
- A book taken from a webnovel site keeps its header out of the translation. These sites write a block above each chapter — the file name as a slug, the address the chapter came from, the site’s handle, the credits of the team that translated it. Libris read it as prose: it was translated, so a delivered book carried a translated source address and a translator’s name in the target language, and it was sent to the model, which refused three passages of a production volume outright because it had been given metadata and no story. Such a header is now given back exactly as it was in every export, and never translated nor analysed. It is recognised in two ways, and either is enough: a line that says what it is (an address, a site handle, a credit —
Source:,Translator:,Traducteur :… — or the file name written out again as a slug), and a line that the chapters of one import share word for word, which is what gives a header away whatever a site calls its fields. Both only look at the lines above the first one that reads like the work, so a title, an epigraph or a line of dialogue shaped like a credit is never taken for one; the shared lines need at least three chapters and eight in ten of them. Nothing is re-read and no model is called: the first lines of each file are counted once per import. A chapter that holds nothing but a header is refused at import, as an empty chapter already was, and books already imported are untouched.
Fixed
- A chapter that gains an analysis is consolidated again. A passage a provider refuses keeps no analysis of its own. Analysing it later did not help: its chapter had already been consolidated, and a Book Bible validated by the autopilot consolidates nothing more, so the passage reached the characters, the relations and the glossary and never the book’s own overview — a production volume was left with a bible written without three of its passages. Storing an analysis for a chapter already consolidated now puts that chapter back to consolidate and reopens a Book Bible the autopilot had validated, with a decision saying why. A Book Bible validated by hand is left alone, and the chapters nobody re-analysed keep their consolidation: only what changed is paid for again.
[0.7.4] - 2026-09-20
A release drawn from a complete run of the autopilot on a real production instance, from the file sent to the book delivered: what the autopilot decided along the way, what an EPUB never said about its own chapters, and a trap in the operator command line that can lock you out of your own installation. No database migration.
Fixed
- Glossary terms decided on what the book actually says. The autopilot accepted « Prologue » → « Prologue », « Interlude » → « Interlude » and a chapter’s own title as terms of the book, because chapter headings and the table of contents were counted like prose; it counted occurrences as raw text, so « rock » was counted inside « rocky »; it accepted « the Warden » → « le Gardien » and « The Keeper » → « le Gardien » independently, giving two characters one name; it accepted « harbour wall » and « the harbour wall » as two terms; and it called all of this a « confiance 1.00 », which measured only how often the source term appeared and said nothing about the translation. Occurrences are now counted as whole words in the narrative text alone, headings and tables of contents excluded; a word only the headings say is rejected as a title; a translation that already renders another term is refused, because two terms sharing one become indistinguishable to the reader; a proposal that repeats an accepted term down to its article is refused; and the decision log says what it measures — « 3 occurrences dans le texte du livre, minimum 2 ». Proposals are decided from the most used form down, so a variant yields to the form the book really uses. An accepted term remains a suggestion in the prompt, never a locked one.
- A Book Bible the autopilot validated no longer freezes a growing book. Validating a Book Bible stops every later analysis from consolidating anything into it — right for a bible a person owns, wrong for a serial followed over time, which kept the Book Bible of its first chapters while the book went on growing (and a validated bible also outranks an open one when the context budget is tight). The autopilot’s own validation is now recorded as its own: chapters added to the volume reopen it, with a decision saying so, and the next analysis consolidates them into it. A Book Bible validated by hand is the reader’s and is never reopened.
- A run that stops its own job is no longer cancelled for it. A run that takes its job out of running — an analysis that produced too little, a volume waiting for an earlier one of its series, a budget reached — looked to its own heartbeat exactly like a job someone else had paused or reclaimed, and the next renewal cancelled a run that was already ending on its own terms. The job kept the state and the reason it had chosen, but the worker raised an
asyncio.CancelledErrorout of a clean stop, which said nothing true about what had happened and hid what did. The heartbeat now cancels a run only when something else took its job: the run records its own stop before the change is visible to anyone, at each of the three places one happens. A pause or a cancellation asked for by a person stops the run at once, as before. librisctl restartsays when it cannot apply a changed configuration. Docker gives a container its environment once, when it is created, so a restart keeps the values the containers were born with: changing the public address of an installation and restarting it left the oldALLOWED_ORIGINSin the API, which answered « Origine non autorisée » to every login from the new address, with nothing in the logs to explain it.restartnow compares the configuration file with the containers’ creation time and says thatlibrisctl upis what applies the current one;doctorchecks the same thing.
Added
- A book sent as an EPUB names its own chapters. The chapter map of 0.7 read file names, the headings of a file split by titles and the chapters of an API request, but never the chapters of an EPUB: a prologue, an interlude and the two parts of a chapter all arrived as plain chapters, with no number, no part and no name, and the completion report’s chapter map was empty for every EPUB. The EPUB adapter now reads each narrative chapter’s heading with the same detector — and only that: the file already gives the reading order, so nothing is reordered. Labels, exports, the follow-up of chapters published in parts and the API now see the same map for an EPUB as for chapters sent one by one; the table of contents and the auxiliary pages stay out of the book’s numbering.
[0.7.3] - 2026-09-20
A fix release for an analysis that could not write its Book Bible: the volume stayed blocked whatever was launched afterwards, and only a correction made by hand in the database got it out. No database migration, no change to the rest of the pipeline.
Fixed
- A Book Bible no run could write is rebuilt by the next one. When every Book Bible synthesis of a run failed — a provider answering prose, an outage at the wrong moment — the run still marked its chapters consolidated, and the volume kept an empty bible. Nothing rebuilt it afterwards: a new analysis consolidated nothing, because its chapters already passed for done, and the guard added in 0.7.2 then stopped the job as
blockedwith « Book Bible vide malgré 79 passages analysés » at every attempt. Three things change. A chapter is marked consolidated only when a batch of its evidence actually reached the bible, so a chapter whose batches were all given up stays to do. A run whose every synthesis was given up records nothing at all: the volume keeps the bible it had instead of having it overwritten by an empty one. And a volume left without any Book Bible is rebuilt whole, chapters already marked consolidated included. - Resuming a job asks again for what was given up. The passages the autopilot left without an analysis and the Book Bible batches that failed were remembered as settled for the whole life of a job: resuming it carried the same holes, so a book blocked for an unusable analysis came back blocked at once. Resume now forgets them and the run that follows asks for them again; everything that succeeded is kept, and automatic resumes after an outage are unchanged.
[0.7.2] - 2026-09-20
A fix release for book analysis: the Book Bible syntheses of a long book came back as questions instead of JSON, and a book could then be translated with no memory at all. Answers that arrive as prose, truncated, or with an extra key are now repaired, and an unusable analysis stops the job instead of translating blind.
Fixed
- Book Bible syntheses that came back as questions. Above the chapter level, the parallel analysis (0.7) named each node of its synthesis tree by chaining the labels of the nodes below it: from the third level on, one call announced eight or more chapter titles in a single
chapterfield while carrying three partial syntheses asevidence, and the model answered with prose asking which unit was meant instead of the JSON object — « Réponse invalide : … Invalid JSON » for everybook_analysiscall, and no Book Bible at the end of the analysis. A node now keeps the range it really covers (its first and last chapter), and every synthesis call, in the parallel tree as in the strict mode, receives the same payload:covers(one chapter or a first-to-last range), the overview to extend, the character registry, andevidencewithevidence_kindsaying whether its entries are passage analyses or partial syntheses of the same book, each with the chapters it covers. Thebook_analysisprompt describes that payload and asks never to request clarification; the built-in prompt files move tofile-v4. - A model that answers prose to a JSON request is asked again. A
200whose body is not JSON at all was counted as an ordinary validation failure and replayed with a message about unit ids, which said nothing to a model that had asked a question. It is now recognised for what it is: the call is retried once, told that the reply must be the JSON object only, and the provider’s JSON mode gives way to the next one (json_schema→json_object→ plain text with the schema in the system prompt) — before the passage or the synthesis is given up. The budget of three full-price attempts per call is unchanged, a valid first answer still costs one call, and the request fingerprint and the response cache are untouched. - An answer is no longer lost because the model added a key. Every model Libris validated a provider answer with refused unknown keys, so a provider that does not enforce the requested schema lost a whole good answer whenever it wrapped it or invented a field — production saw
chapter_extractionrejected for « Extra inputs are not permitted » onanalysisandunits— and under the autopilot the passage was skipped. Answer models now ignore keys Libris did not ask for, while every field Libris reads keeps its validation: a summary that is missing, or a character without a canonical name, is still an invalid answer. The schema sent to the provider is unchanged and still closed (additionalProperties: false), and the request models of the API still refuse unknown keys. - An answer cut at the output budget is repaired instead of abandoned. A JSON object that stopped in the middle — the provider’s
finish_reason: length, or an object that simply never closed — was a dead end: the passage or the synthesis was given up at once. Such an answer is now asked for one more time, keeping the provider’s JSON mode (the format was understood, only the length was wrong) and asking for the same object complete but much shorter. The log of the failed request names the cause and the provider’smax_output_tokens, so the budget can be raised. A provider that keeps running out of room costs one repair, not the three full-price attempts, and a valid first answer still costs a single call. - A book is no longer translated without its analysis. After an analysis where every model call failed, the autopilot skipped all 81 passages and the 10 Book Bible syntheses, then translated 77 of those passages anyway: no chapter analysis in memory, an empty Book Bible, and a whole book to translate again. An analysis now looks at what it left before the translation stage: with no analysed passage, with a share of analysed passages below
ANALYSIS_MIN_COVERAGE(new setting,0.5,0disables it), or with an empty Book Bible nobody validated, the job stops asblockedwithstop_reason = analysis_unusable, a decision in the autopilot log and a message naming what failed and what to do — run the analysis again, try another provider, or validate a Book Bible completed by hand. A few skipped passages degrade exactly as before and the translation goes on.
Added
librisctl, one command line for a production host. Administering the installation meant remembering the Compose project, its environment file, the Codex profile and longdocker composelines.deploy/librisctl, installed as/usr/local/sbin/librisctl, has one command per task:status(version, images, containers, health, volume sizes, free space, last dump),ps,logs,start,stop,restart,up,health,version,jobs(a read-only view of the queue, ids only),shell,psql,backup,restore,rollback,prune-images,check-composeanddoctor, which checks in one pass the configuration, the containers and their hardening,/health, Compose drift, free disk, a recent dump and the worker’s leases. Its output is plain text without colour or pager, so it reads the same over SSH and in a log; it never prints the configuration or a key, never removes a container or a volume, and asks for--confirmbefore restoring a dump, stopping the database or removing images. Exit codes are documented:0success,1a check failed,64wrong usage,65production not provisioned or a precondition not met,77refused. Each deployment refreshes it from the image it deploys, so it is always the version running. See operations.
Changed
- The production configuration moves next to the rest of the installation. The deployment, backup, restore and
librisctlscripts took the installation’s.envat/opt/epub-translator/.env, away from the Compose file, the recorded version and the dumps. They now resolve it in three steps: the explicit variable (LIBRIS_PRODUCTION_SECRET_ENV,LIBRIS_BACKUP_SECRET_ENV,LIBRIS_RESTORE_ENV_FILE), then$LIBRIS_PRODUCTION_BASE/.env, then the legacy path, which is still used and reported with a one-line notice.librisctl migrate-env --confirmmoves the file with its permissions and says what to restart. Nothing to do on an existing installation until you choose to move it. - The Compose project name is configurable. The Compose file takes it from
LIBRIS_PROJECT(stillepub-translatorby default), and the scripts read$LIBRIS_PRODUCTION_BASE/projectwhen the environment does not name one. The default is unchanged: renaming a project means new, empty volumes, solibrisctl migrate-project [NAME] --confirmdoes the move instead — it refuses while a job is active, stops the installation, copies each volume into its twin, compares their sizes, starts the installation under the new name and records it, leaving the old volumes untouched for a return.
[0.7.1] - 2026-09-20
A maintenance release: managing archived books, the wording and language of the interface and the operator scripts, a job that could end as cancelled the moment it finished, and backend test suites five times faster. No database migration, no behaviour change to the pipeline.
Fixed
- Priority of TXT and DOCX uploads. The
priorityoption of a file upload to/api/v1/translation-requestswas ignored for text chapters: the request always ran at normal priority and a token could ask for more than its ceiling without the403 priority_not_allowedanswer. It now applies like for a JSON request or an EPUB. - One word for the language model service in French. The French interface and API messages mixed « provider » and « fournisseur »; they now say « provider », as the French user guide does (for example Providers de secours). English is unchanged.
- Every importable format on the empty library. The empty library screen listed EPUB and TXT chapters only; it now also names Markdown, HTML and DOCX chapters.
- OpenViking in the series deletion dialog. Deleting a series said nothing about its remote OpenViking memory. Every deletion dialog (series, book, selected books) now tells an administrator whether the OpenViking documents will be removed or kept, from the cleanup switch; other accounts, who cannot read it, keep the general rule, and nothing is said when no OpenViking URL is set.
- Priority menu for editors of a shared book.
PUT /api/queue/{job_id}/priorityaccepts the editors of a shared book, but the Queue page only offered the menu on the caller’s own books. Queue entries now carryeditable, and the menu follows it, within the account’s ceiling. - English operator output of
scripts/setup.py. Its only message was in French. - Cost estimate explanations in English. The basis and price note of
GET /api/projects/{id}/estimate(for example « Aucun provider n’est associé à ce livre… » or « Historique de 2 livres avec ce provider… ») and the price note ofGET/PUT /api/projects/{id}/budgetwere always French. They now followAccept-Languagelike the other API messages, with correct singular and plural forms in both languages; French stays the default. - English output of the operator scripts.
scripts/enable_codex.py,scripts/smoke.py,scripts/resilience_smoke.pyandscripts/cleanup_browser_fixtures.pystill printed French messages; they now print English. Their behaviour is unchanged. - Managing an archived book. An archived volume of a series appeared nowhere: the library’s Archives filter only listed standalone books, and the series page hid it, so the only way to reach it was to detach it from its series first — and once detached it still could not be selected, since archived books were the one thing the checkboxes refused. The Archives filter now lists every archived book, standalone or volume, each with its series name and number, and the series’ Volumes tab reveals its archived volumes on demand (Show N archived volumes, hidden by default). Archived books are selected like any other, alone or with the header checkbox, and the batch bar offers what applies to a book at rest — Restore selection, Export EPUBs, Delete selection — while the actions that would call the model (Analyze, Translate, pause, resume, cancel) are removed or disabled with their reason, so an archived book never starts paid work. The row menu of a volume adds Restore and, for an archived one, Delete, without detaching it.
- Volumes counted by a series. A series counted its archived volumes in its own totals, so the library announced more volumes, chapters and translated passages than its page showed once the archived volumes were set aside. The totals, formats, providers, memory and chapter list of an active series now cover its active volumes only; an archived series keeps all of its own. An archived volume still holds its number in the reading order: it is neither reported as a missing volume nor is its number free for another volume.
- A finished job no longer ends as a cancelled task. When a lease renewal fell right after a job was marked completed, the heartbeat saw the job as stopped and cancelled the worker task that had just finished it. The job stayed
completed, but the worker handled it as an interruption. The heartbeat is now stopped before the completion is written.
Changed
- Faster test suites. The backend suites build their schema once and empty the tables between tests, and run on several processes (
pytest -n), each with its own database; the SQLite suite goes from about 5 to under 1 minute and the PostgreSQL one from about 8 to under 2 on a 16-core machine.docs/development.mdexplains how to run them.
[0.7.0] - 2026-09-19
Upgrading. Eight database migrations run at start-up after 0.6.0 (c4d1a8e27b63; alembic upgrade head, automatic in the Docker deployment): passages kept in the original are no longer human corrections (9d3e5b1f7a24), chapter progress webhooks (4b8e1c6d2f90), OpenViking cleanup log (a3f6c1d82e57), fair queue (300863c7cbc7), shared glossaries (3c8e1a5d2f47), passage quality scores (e4b7c2a91d35), cost budgets (4b8e2d6f1c93) and the chapter map (7c2d9e4a1b58). Apart from the human flag of retained passages and the role, part and label given to existing chapters (a prologue or epilogue that a split by headings had numbered keeps that number in import_meta.legacy_number, restored by a downgrade), they only add tables, columns and indexes: no passage text or translation is changed, and nothing is translated again. Things to know:
- Behaviour change — fair order instead of first come, first served. The worker no longer starts waiting jobs strictly oldest first: it takes the highest priority, then the account and the API token with the fewest jobs running, in turn between accounts, and a job that has waited
QUEUE_PRIORITY_AGING_MINUTES(60) rises one level. With one account and default priorities, jobs still start in the order they entered the queue (a resumed job enters it again), apart from the turn taken between API tokens. Existing jobs becomenormalpriority; existing API tokens may ask fornormalat most until theirmax_priorityis raised. - Behaviour change — follow-up chapters are translated alone. Chapters sent to a volume already translated are no longer followed by a final review and a consistency check of the whole volume: only the new or replaced chapters (and any chapter still missing a translation) are worked on.
- Behaviour change — secure session cookie by default (see Changed): an installation opened over plain HTTP on a network address whose
.envhas noCOOKIE_SECUREline must addCOOKIE_SECURE=false. - Behaviour change — parallel analysis by default. A volume’s analysis now runs its passages side by side and reconciles each one with what precedes it (
ANALYSIS_MODE=parallel): much faster on long serials, with about 1.8 times more analysis calls (translation calls are unchanged).ANALYSIS_MODE=strictrestores the previous passage-by-passage analysis. - Nothing new is on by default. No budget applies until one is set (
BUDGET_DEFAULT_BOOK=0), no queue quota until one is set (QUEUE_MAX_RUNNING_PER_ACCOUNT=0,QUEUE_MAX_QUEUED_PER_ACCOUNT=0), and OpenViking documents are only removed on deletion once an administrator switches the cleanup on (OPENVIKING_CLEANUP_ON_DELETE=false). - New settings, all optional: budgets (
BUDGET_DEFAULT_BOOK,BUDGET_SWITCH_THRESHOLD,BUDGET_ON_ESTIMATE, also in Settings › Budgets), fair queue (QUEUE_MAX_RUNNING_PER_ACCOUNT,QUEUE_MAX_QUEUED_PER_ACCOUNT,QUEUE_PRIORITY_AGING_MINUTES, also in Settings › Queue with per-account quotas) OpenViking cleanup (OPENVIKING_CLEANUP_ON_DELETE, also in Settings › Memory · OpenViking) and analysis (ANALYSIS_MODE,ANALYSIS_RECONCILIATION). Values saved in the interface override the environment until they are reset. - New automation API answers a client may meet:
402 budget_exceeded(a token’s cost budget is reached),403 priority_not_allowedand429 queue_full(fair queue), and new fields in the status document (priority,queue,chapters.new,chapter_events) and in the completion report (cost,quality). The published OpenAPI description (docs/openapi/libris-v1.json) describes them all.
Added
- Cost budgets per book. Libris showed an estimate before a launch, but nothing stopped a book from costing far more than planned: a long book on an expensive model, or an autopilot that kept recovering passages, spent until the job ended. A book now has a spending cap, in the currency the provider prices are entered in: its own (Book budget card at the top of the book’s Settings tab) or the installation default (
BUDGET_DEFAULT_BOOK, or Settings › Budgets). It covers everything the book has cost, all jobs included. Before a launch, the confirmation shows what is left of it next to the estimate; an estimate above what is left starts the job with a warning, or is refused (BUDGET_ON_ESTIMATE=refuse), and a cap already reached refuses any launch. Before every model call, a job that reaches the switch threshold (BUDGET_SWITCH_THRESHOLD, 90 % by default) moves to the first cheaper provider of the book’s or the installation’s fallback chain; with none, it pauses with a clear reason (stop_reason = budget_exceeded, a Budget reached: job paused banner with Raise the budget), and resuming is refused until the cap is raised. Every switch or pause is written to the decision log (stagebudget). The API isGET/PUT /api/projects/{id}/budgetandGET/PUT/DELETE /api/settings/budget; the estimate (GET /api/projects/{id}/estimate) carries abudgetblock. - Cost budgets per API token. An integration could spend without limit. A token may now have a cap per calendar month or over its whole life (Token budget when creating it, Change the budget in the list, which also shows what the current period spent;
budget_amount/budget_periodinPOST /api/tokens,PUT /api/tokens/{id}/budget). Once it is reached, a request that would start work gets402 budget_exceededwith the cap, the spend, the period and when it resets; an import alone is still accepted. A running request whose token nears its cap switches provider or pauses like a book, and…/resumeanswers409 budget_exceededuntil the cap is raised. - Estimated against real cost in the reports. Nothing compared what a job was expected to cost with what it cost. The completion report of an automation request and the autopilot report (
GET /api/projects/{id}/autopilot) now carrycost: the estimate made at launch, the real cost, the book’s budget and spend, the launch warning, whether a budget paused the job and how many times it moved to a cheaper provider. The book’s Book budget card shows the same for its last job. - Follow a webnovel over time. Translating a series as it is published meant sending each new batch of chapters and then paying for the whole volume again: the final review and the consistency check re-read every chapter already delivered, and could rewrite them. New chapters sent to a volume that is already translated (automation API or interface import) are now appended by number, deduplicated by
external_idor checksum, and translated alone with the earlier chapters as context: the job covers only the new or replaced chapters (and any chapter still missing a translation), earlier ones get no model call.volume.latest: true(orvolume=latestfor TXT uploads) targets the series’ last volume without tracking its number. - Updated results, whole volume or new chapters only.
GET /api/v1/translation-requests/{id}/resulttakesscope=request|new|volume: the chapters the request sent (default), only those it added or replaced, or the whole updated volume. The status document lists the new chapters inchapters.new. In the interface, the export options of a text volume take a chapter range (From chapter / To chapter) for the ZIP, text and Markdown exports. chapters.translatedwebhook. A request that lists it incallback_eventsgets one signed webhook per batch of chapters translated while its job runs, before the finaltranslation_request.finished, with the same allow-list, signature and retries; the status document shows the batches inchapter_events.- Optional cleanup of OpenViking memory when a volume or series is deleted. Deleted books used to leave their documents on the OpenViking server forever. Administrators can now switch on the cleanup (
OPENVIKING_CLEANUP_ON_DELETE, or Settings › Memory · OpenViking › OpenViking cleanup; off by default). When it is on, deleting a volume or a series queues the removal of its OpenViking directories. The deletion itself stays immediate, even when OpenViking is down. The worker then does the removal, retries it with a growing delay and picks it up again after a restart. Libris only removes a whole directory of the deleted item under the configured root, and only after the database confirms at that moment that nothing lives there any more. A log shows what was removed, document by document. - Clean up OpenViking orphans. A dry run in the same card lists the directories of volumes and series that were deleted or moved before the cleanup was switched on, with the reason. Nothing is removed until the administrator confirms, and each directory is checked against the database again before removal.
- Fair multi-user queue. On a shared installation, one person (or one integration) launching a large backlog used to hold everyone else back, because jobs started strictly oldest first. The worker now picks the next job by priority, then from the account and the API token with the fewest jobs running, in turn between accounts; a job that waits long enough rises one priority level (
QUEUE_PRIORITY_AGING_MINUTES, 60 by default), so low-priority work is never starved. Provider capacity, leases and checkpoints work as before. - Priorities. A launch can be
low,normalorhigh(priorityinPOST /api/projects/{id}/jobs,pipeline.priorityor thepriorityupload option in/api/v1). High priority is for administrators and for accounts they allow; an API token has its own ceiling (max_priority). Asking above it answers403 priority_not_allowed. - Quotas per account and per token.
QUEUE_MAX_RUNNING_PER_ACCOUNTmakes an account’s extra jobs wait for their turn;QUEUE_MAX_QUEUED_PER_ACCOUNTrefuses new launches and API requests beyond it with a clear429 queue_full. Settings › Queue overrides both at run time, per account too, and API tokens can have lower limits of their own (max_running,max_queued, set at creation or withPUT /api/tokens/{id}/queue). - Queue page. A new Queue page lists running and waiting jobs with each one’s place in its provider’s line and why it waits (provider busy, account or token limit, retry scheduled, no provider), and lets people change a job’s priority within their ceiling. A book whose job waits says so on its page, and the
/api/v1status document gainspriorityandqueue(position,reason). - Bilingual EPUB for proofreading. Checking a translation on an e-reader meant switching between two books. Every volume, whatever its source (EPUB, TXT, Markdown, HTML, DOCX or JSON), now exports a bilingual EPUB where each source paragraph comes with its translation, chapter by chapter, with a table of contents: interleaved (the original, then its translation) or side by side (two columns that stack on a small screen). Each text carries its language and direction; the book is checked by EPUBCheck. It is in the Export menu and Export options… (with the layout and the option to fill in untranslated passages, shown with their source and an empty translation), at
GET /api/projects/{id}/export/epub-bilingual?layout=…, and in the automation API as theepub-bilingualresult format (?format=epub-bilingual&layout=side-by-side, or as the request’soutput_format). - Shared glossaries for a universe. Series set in the same universe (spin-offs, prequels, a webnovel and its side stories) each had to repeat the same place names, titles and spells in their own glossary, and nothing kept them in step. A shared glossary is now a named terminology of one account that several series can follow (Shared glossaries in the sidebar; the series’ Glossary tab chooses the one it follows). Its accepted terms reach every volume of those series with a clear precedence: the book, then the series (a person’s series decisions, then earlier volumes), then the shared glossary. A locked shared term is enforced and checked in every passage like a locked book term and beats an unlocked term an earlier volume proposed; a volume’s deliberate override or a series decision always wins. The autopilot withdraws a proposed book term that contradicts a locked series or shared term. Optional languages keep a glossary to volumes of the same language pair. In a series, the book’s Glossary tab lists the Terms applied to this book with the level each comes from and what it replaces (
GET /api/projects/{id}/glossary/effective). The automation API manages them too:GET/POST /api/v1/glossaries,GET /api/v1/glossaries/{id},…/export/{format},…/import(?dry_run=trueto preview) andGET/PUT /api/v1/series/{id}/shared-glossary. - Glossary import preview. Importing a glossary applied it at once and never replaced a term already present, so a corrected spreadsheet could not update a book, and a file with a strange header or one bad row was simply refused. Every import (book, series or shared glossary) now opens a preview first: detected format, encoding (UTF-8 with or without a byte order mark, Excel’s Windows encoding) and separator (
;,,or tab), the column holding each field (editable when the headers are not recognised, with The first row holds the column names), then the new terms, unchanged terms, conflicts with the terms in place, duplicates in the file and invalid rows. On conflict keeps the terms in place (default), replaces unlocked terms, or replaces everything including locked terms; invalid rows are left out once shown. The series glossary gains the same import and export. The API accepts the same options (strategy,delimiter,mapping,header,skip_invalid) and answers the report;POST /api/projects/{id}/glossary/import/previewand/api/series/{id}/glossary/import/previewpreview without writing. - Glossary export for spreadsheets. Export › CSV (spreadsheet, semicolon) writes semicolons and a byte order mark so that Excel opens accents and columns correctly (
?delimiter=semicolon&bom=trueon every glossary export route). - Quality score for every passage. A finished book gave no way to know where to start proofreading: the alerts, critiques, doubts and autopilot decisions were spread over several tabs, and many were closed automatically. Each translated passage (and each passage kept in its original) now has a score from 0 to 100, computed without any model call from the signals Libris already records — unresolved alerts and locked-term violations, open critiques and doubts, unusual length, failed model calls, recovery steps and arbitrations, points the autopilot closed without a correction, retained originals — and stored with the signals that lowered it. It is recomputed in the same transaction whenever the passage or one of those signals changes; books translated earlier are scored the first time they are viewed. A passage validated by a person scores 100.
- Quality dashboards for books and series. The book’s Quality tab and a new series Quality tab show the average, the distribution by band (good, fair, weak, poor), the passages to review first with the reasons, each opening in the editor (from a series too, through
#project/<id>/passage/<segment>), and the chapters ranked weakest first; the series view adds each volume’s figures. The editor shows each passage’s score, and Summary & recovery a quality summary. - Quality in the reports. The autopilot report and the automation API’s completion report carry a
qualitysection (distribution, passages to review, and for API requests the weakest chapters and passages of the request). - Published OpenAPI description of the automation API. Integrators had to read docs/api.md to write a client, with nothing an OpenAPI tool or a client generator could load.
docs/openapi/libris-v1.jsonnow describes/api/v1only (never the interface’s session API) in OpenAPI 3.1: every operation with its scope, parameters, the JSON, multipart and raw EPUB bodies, the answers, error codes with the shared{"detail": {"code", "message"}}schema, examples, and the signed webhook. It is generated from the code bypython3 scripts/export_openapi.py; the test suite fails when the committed file is out of date and checks the documented answers against real ones. The website shows it as a readable reference (https://libris-translate.com/api/reference/) and serves the file at/openapi/libris-v1.json. - Example client with nothing to install.
examples/libris_client.py(Python 3.10+, standard library only) sends an EPUB, TXT chapters or a JSON document, long-polls until the request ends, downloads the result, waits out429answers, and receives webhooks after checking their HMAC signature. It works as a command or as a class to import, and is tested against the real API. docs/api.md links both. - Providers listed to API tokens. A script with only a token could not find the provider ids a translation request names in
provider_id: the list was only served to the interface’s session.GET /api/v1/providers(scopecontent:write) returns each provider’s id, name, kind, model and the caller’s series that use it by default, never its address or key. - Restoring a prompt version or the built-in prompt. Settings › Prompts could only create new versions: going back to an earlier text, or to the prompt shipped with Libris, meant copying it by hand. The page now shows the Version history of the selected prompt (date, version in force, content) with a Restore button, and Go back to the original prompt; both ask for confirmation. Restoring saves the chosen text as a new version, so nothing is erased; going back to the original follows the built-in prompt, including its updates in later releases. The API is
GET /api/prompts/{name}/versionsandPOST /api/prompts/{name}/restore(administrators only, like editing). - Resetting the automatic recovery delay. Once a delay was saved in Settings › Automatic recovery,
PROVIDER_RECOVERY_BASE_SECONDSwas ignored for good, and with nothing saved the page showed 60 seconds whatever the environment said. The page now shows where the delay comes from (saved here or environment) and the environment value, and Go back to the environment delay forgets the saved one, like the autopilot and webhook pages.GET /api/settings/recoveryanswersretry_seconds,default_secondsandsaved;DELETEresets. - Parallel analysis, quality first. Analysing a long webnovel passage after passage took as long as
the provider’s latency times the number of passages (a 400-chapter serial: hours before the first
translated line). The analysis of a volume now runs its passages side by side (
ANALYSIS_MODE=parallel, the default): each passage is first extracted on its own, the extractions are consolidated in book order, then every passage is reconciled, in parallel too, with what the passages before it established (identities and aliases, the characters named last for pronouns, relations, terms, the summaries of the passages before); the memory is then written in book order by the same code as before, and the Book Bible is built as a tree. Nothing a later passage reveals is shown to an earlier one, the result is the same whatever the number of threads, every stage resumes without asking the model twice, and translation starts only once the whole analysis of the volume is done. A numbered volume of a series waits, before its reconciliation, for an earlier volume still being analysed (queue reasonearlier_volume, bounded byAPI_REQUEST_STALL_MINUTES). On a synthetic serial with known ground truth the parallel mode keeps every score of the strict mode and resolves far more pronoun references; on a 400-chapter serial with 16 calls at once the analysis ends about 5 times sooner, for about 1.8 times more analysis calls. The strict mode stays available (ANALYSIS_MODE=strict, Analysis mode in the book settings,analysis_modeof a launch or of an API request);ANALYSIS_RECONCILIATION=flaggedreconciles only ambiguous passages. - One threads knob. A volume (Passages worked on at once in its settings), a launch (
threads) and an automation request (pipeline.threads, upload optionthreads) choose how many passages are in flight at once, for the analysis and the translation. It can only lower the book’s share of the provider’s capacity, so one big book never starves the others. After a provider answers429or is overloaded, the job resumes at half its width and widens again by one passage per minute; near a cost budget, the calls in flight are reserved before they start and the job narrows down to one call at a time. - Analysis progress. The interface shows the step of a running analysis (extraction i/N, consolidation,
reconciliation i/N, memory writing, Book Bible level k/K), and so do
analysis_phasein the book andprogress.analysisin the/api/v1status document. - Split one file into chapters by headings. Webnovels often come as one big TXT, Markdown or DOCX file,
which Libris imported as a single chapter. The import assistant now finds the chapter headings (Word heading
styles, Markdown
#titles, lines such as “Chapter 12”, “Chapitre 12 : Titre”, “CHAPTER XII”, “第12章”, “Prologue”, or consecutive “12. Title” lines), skips a table of contents and sentences that merely mention a chapter, and proposes “Split into N chapters” with each chapter’s number, title, size and first words: rename, renumber, merge a chapter with the previous one or keep the file whole. Text before the first heading becomes a front matter chapter. Each part is imported exactly like a separate file named with its number (same numbering, deduplication, exports and project archives), and a file too long for one chapter can now be imported split. The automation API accepts DOCX chapters and asplit=headingsoption that applies the split without preview and records it in the report. - Evaluation tools.
scripts/evaluate_analysis_modes.pyscores the memory of each analysis mode against a synthetic ground truth;scripts/benchmark_analysis.pymeasures wall time, calls and tokens per mode and thread count. - Chapter map for irregular webnovels. Webnovels publish chapters in two parts, add prologues,
interludes, side stories, bonus chapters, afterwords and author’s notes between numbered chapters, and
Libris only knew chapter numbers: a part 2 collided with its part 1, and a prologue or an interlude got
an invented number (
0,5.5). Each chapter now has a kind (chapter,prologue,interlude,side_story,extra,epilogue,afterword,author_note,front_matter), an optional number (a special’s own:Interlude 2), an optional part and part count, a label (AylainInterlude – Ayla) and a reading position that orders the volume independently of the numbers. They are detected, with a confidence and a reason, from file names and headings in the usual English, French, Spanish, German, Chinese, Japanese and Korean forms (Chapter 12 - Part 1,Ch12 (2-2),Chapitre 12 partie 2,12a/12b,12.1/12.2sent together,第12章(上)/(下),Prologue,序章,Interlude – Ayla,Side Story 3,SS3,番外,Bonus Chapter 2,Afterword,あとがき,Author's Note,Illustrations…); a lone12.5stays a real half chapter. - Map in the import assistant. For TXT, Markdown, HTML and DOCX chapters, each row shows the kind, part, part count, name and, for an existing volume, Insert after; rows placed only from the alphabetical order of the files are flagged; bulk actions make the selected rows the parts of a chapter, give them a kind or a place. Everything works with buttons and the keyboard, on phones too. Confident maps are applied as proposed. Files split at their headings get the same fields per part.
- Edit the map after the import. On the series Chapters tab, a chapter’s kind, number, part and
name can be corrected, and with a volume selected, Move up / Move down move it in the reading
order: its passages follow with what the memory learned from them, and the chapters whose preceding
context changed are marked for a new check. Session API:
PUT /api/projects/{id}/chapters/{cid}/map,POST /api/projects/{id}/chapters/{cid}/move. - Chapter map in
/api/v1. Chapters of a JSON document takekind,part,part_count,label, andafter(a chapter id, an external id, a chapter number orstart) orposition; missing fields are read from the title, and TXT/DOCX uploads read them from the file names. The status document and the JSON result give each chapter’s map,display_labeland readingposition; the completion report lists the map inchapter_map. An unknownafteranswers422 invalid_placement. Documents without the new fields keep their checksum and their order. - Join the parts of a chapter in exports.
merge_parts=true(export dialog Join the parts of a chapter, text formats and bilingual EPUB,POST /api/exports/text) puts the parts of a chapter under one heading; storage stays one chapter per part.
Changed
- Secure session cookie by default.
COOKIE_SECUREnow defaults totruein the code, as it already did in the generated.env. An installation whose.envhas noCOOKIE_SECUREline and is opened over plain HTTP on a network address must addCOOKIE_SECURE=false; HTTPS andhttp://localhostare unaffected. - Database migration
9d3e5b1f7a24clears thehumanflag of passages kept in the original (see Fixed); it runs at start-up like the others. - Database migration
4b8e2d6f1c93adds the token budget (api_tokens.budget_amount,budget_period), the cost kept on each ended automation request (translation_requests.cost, so token budgets still count it once the model calls are purged) and an index on the token of requests; it runs at start-up like the others. - Delete confirmations and OpenViking. Delete confirmations no longer say that remote OpenViking memory is always kept. They now say it is removed only when an administrator has switched the cleanup on.
- Database migration
3c8e1a5d2f47adds the shared glossary tables (shared_glossaries,shared_glossary_terms,series_shared_glossaries); it runs at start-up like the others and changes no existing row. - Book glossary import answer.
POST /api/projects/{id}/glossary/importstill answersimportedandskipped(same meaning, and still keeps existing terms by default) and now addsreplacedand the import report. - Database migration
4b8e1c6d2f90adds the progress webhooks of automation requests (webhook_events, one row per batch of translated chapters); it runs at start-up like the others and changes no existing row. - Database migration
a3f6c1d82e57adds the OpenViking cleanup log (openviking_cleanups), empty until the cleanup is switched on or orphans are cleaned; it runs at start-up like the others. - Database migration
300863c7cbc7adds the queue fields of jobs (priority,token_id,queued_at,claimed_at) and the queue limits of API tokens (max_priority,max_running,max_queued); existing jobs become normal priority, queued at their creation time, and existing tokens may ask for normal priority at most. - Database migration
e4b7c2a91d35adds the passage quality scores (passage_quality); it runs at start-up like the others, and passages translated earlier are scored the first time their book’s quality is read. - Specials keep their place instead of an invented number. A volume split at its headings, a batch of files and an API request place prologues, interludes, epilogues and other specials by the chapter they follow (a prologue first, an epilogue or afterword last) instead of numbering them between their neighbours; a part 2 or an interlude sent later lands right after its chapter, and a new chapter goes before a closing epilogue. Context, memories, no-spoiler rules, parallel analysis, quality rankings, follow-up scopes and exports all follow the reading position.
- Exports name chapters by their map. A chapter whose title only gives its number or kind is headed
by its label in the target language (
Chapitre 12 (partie 2),Prologue,Interlude – Ayla) in the text, Markdown, ZIP and bilingual EPUB exports and in the table of contents; the bilingual EPUB marks prologues, epilogues and afterwords with their EPUB semantics. A ZIP of a volume with parts or specials numbers its files in reading order, and its manifest gives each chapter’s kind, part and label. Chapter ranges (From chapter / To chapter) cover the parts of those chapters and the specials between them. - Existing chapters are mapped by the migration. Chapters whose title plainly names a special become
that special (a number the split had invented is cleared and restored by a downgrade), and titles such
as
Chapter 12 - Part 2orChapter 12 (2/2)give their part.
Fixed
- Updating with the installer. Running
./scripts/install-docker.shagain after an update restarted the version already written in.env, unlessLIBRIS_IMAGE=was given by hand. An officiallibris:<x.y.z>pin older than the release shipped with the files is now moved to it (and the installer says so); a newer release, a custom image and an explicitLIBRIS_IMAGE=still win. - Session cookie defaults. The code defaulted
COOKIE_SECUREtofalsewhile.env.examplesettrue, and its comment claimedfalsewas needed onhttp://localhost:8088. Browsers accept Secure cookies onlocalhost; only plain HTTP on a network address needsfalse. The code now defaults totrueand.env.example, the configuration reference and the Docker guide explain the network case. - Spaces in
ALLOWED_ORIGINS.http://a:8088, http://b:8088kept the space before the second origin, whose requests were refused with 403. Spaces around each origin and empty entries are now ignored. - Provider comparison report path.
compare_providers --output report.jsonfailed in the read-only container. A relative path is now written underDATA_DIR/tmp, and the command prints where the report went. - Database migrations under the previous worker.
scripts/deploy.shran the migrations before looking at the worker, so--api-only(and the other modes, before draining the queue) changed the schema under a worker of the previous version.--api-onlynow refuses with status 5, changing nothing, when a migration is pending and the worker runs;--worker-when-idlemigrates only once the queue is drained and the worker stopped, and--force-workerstops the web application and the worker before migrating. - Startup errors in English. The
SECRET_KEY,BOOTSTRAP_PASSWORD,API_WEBHOOK_SECRETandMETRICS_TOKENstartup errors were French only, unlike the logs and the operator documentation. - Invalid-answer problem. A passage given up after invalid answers said “five invalid answers” while Libris stops after three. The number now comes from the limit itself, and the problems list of a book is shown in English to English-speaking users.
- Passages kept in the original. Keeping a passage’s source text marked it as a human correction: it counted in
report.passages.humanand no later translation could replace it, even when asked explicitly. It is no longer a human correction: the automatic passes still leave it alone, but retranslating the passage or selecting it in Summary & recovery (now listed there) replaces it when the translation succeeds and settles its warning; a failed retry keeps the original with the new error. ?wait=limit. The long-polling parameter accepted up to 3600 seconds but was cut toAPI_RESULT_MAX_WAIT_SECONDS, itself at most 600. Values above 600 are now refused with 422 on both routes.- API token hint in the import assistant. The automation note told users who are not administrators that an administrator creates API tokens, although every account creates its own. It now says so for everyone and links to My account › API tokens.
[0.6.0] - 2026-09-19
Libris becomes an automatic translation service organised as a library of series. An EPUB, TXT chapters or a JSON document go in — through the interface or the automation API — and the translated book comes out, with no human step required: the autopilot handles failed passages, reviews, AI suggestions, provider outages and memory decisions on its own, logs every decision and returns a report. Series, webnovel chapters and earlier volumes feed the context of later ones without ever revealing what comes after.
Upgrading. Four database migrations run at start-up (alembic upgrade head, automatic in the Docker deployment): series library and source files (5e7b0c1d9a42), autopilot decisions and job results (7c4e2a9b1f30), API deliveries (7c4e2a9d1b63) and daily usage aggregates (c4d1a8e27b63). Existing books are grouped into series by owner and normalized series name, every EPUB gets a source-file row pointing at its existing file (nothing is read, moved or rewritten), and every project, chapter and passage identifier is kept: nothing is translated again. Things to know:
- Behaviour change — the autopilot is on by default for whole-book jobs started from the interface, the import or the API. A book reaches an output without anyone validating it; passages that still fail after the recovery ladder keep their source text (
source_retained, with the reason) instead of blocking the export. Turn it off globally (AUTOPILOT_ENABLED=falseor Settings › Autopilot), per book, or per launch. - Behaviour change — imports no longer ask for confirmation: uncertain volume or chapter numbers are applied with their reason and the pipeline starts by default.
IMPORT_CONFIRM_LOW_CONFIDENCE=truerestores the confirmation. - Prompt order changed (stable prefix first, for the providers’ prompt caches): Libris’ own response cache misses once for books in progress, whose requests are made again at their first resume after the update.
- With OpenViking configured, the worker rewrites every event of the volumes that use it into the new per-series layout on its first run, from PostgreSQL; nothing remote is deleted, and until a volume is rewritten its remote hits are ignored in favour of SQL memory (see
docs/openviking.md). - New data directories:
DATA_DIR/sources(TXT, JSON, Markdown, HTML and DOCX sources — back it up with the books; the scheduled backup already archives the whole volume),DATA_DIR/results(delivered API results, rebuilt on demand, removed afterRETENTION_RESULTS_DAYS) andDATA_DIR/staging(uploads waiting for confirmation). - New settings, all optional: imports (
IMPORT_MAX_FILES,IMPORT_MAX_SESSION_MB,IMPORT_SESSION_HOURS,IMPORT_CONFIRM_LOW_CONFIDENCE,TEXT_CHAPTER_MAX_CHARS,PASSAGE_MAX_CHARS), cost (REVIEW_MODE), autopilot (AUTOPILOT_ENABLED,AUTOPILOT_MAX_ROUNDS,AUTOPILOT_FALLBACK_PROVIDERS,AUTOPILOT_OUTAGE_MAX_RETRIES,AUTOPILOT_OUTAGE_MAX_WAIT_SECONDS,AUTOPILOT_GLOSSARY_MIN_CONFIDENCE,AUTOPILOT_IDENTITY_MIN_CONFIDENCE,AUTOPILOT_BIBLE_MIN_COVERAGE,AUTOPILOT_STALE_MIN_COVERAGE), automation API (API_MAX_PAYLOAD_MB,API_MAX_CHAPTERS,API_RATE_LIMIT_PER_MINUTE,API_RESULT_MAX_WAIT_SECONDS,API_REQUEST_STALL_MINUTES,API_REQUEST_MAX_HOURS,DELIVERY_REPAIR_ATTEMPTS,API_WEBHOOK_HOSTS,API_WEBHOOK_PRIVATE_NETWORKS,API_WEBHOOK_SECRET,API_WEBHOOK_MAX_ATTEMPTS,API_WEBHOOK_TIMEOUT_SECONDS) and retention (RETENTION_RESULTS_DAYS,RETENTION_REQUEST_ROWS_DAYS, off by default). Autopilot and webhook values can also be changed at runtime by an administrator; saved values override the environment. - The production Compose file is now versioned in the repository, with hardened containers; Python dependencies are installed from a hashed lock file.
- Project archives are now version 3 (EPUB, TXT and JSON volumes); versions 1 and 2 are still read, but 0.5 cannot read a version 3 archive.
alembic downgradeto 0.5 keeps TXT and JSON volumes in the tables, but 0.5 cannot export them.
Added
- Autopilot: from an input file to an output with no human decision. The pipeline stopped or waited for a person in many places: one recovery pass on the same provider, no fallback provider, refusals blocking the analysis, critiques, doubts and consistency issues left “to check”, glossary, series identities, Book Bible and stale chapters never decided, jobs “completed” with refused or untranslated passages. Whole-book jobs now run a bounded convergence loop (
AUTOPILOT_MAX_ROUNDS, 3): a recovery ladder for failed passages (informed retry, small-batch repair, sentence split, reduced context, fallback providers, then source kept with every attempt as reason), consistency, final review and an AI arbitration of critiques, doubts and inconsistencies at every quality level — a change is applied only if it passes validation, the checks and the locked glossary, and passages edited by a person are never touched. Provider outages wait a bounded time then move to the next provider (job → book →fallback_provider_ids→AUTOPILOT_FALLBACK_PROVIDERS); refusals degrade a step instead of stopping the book. Memory decisions (glossary, series identity links — never merges —, Book Bible validation, stale chapters) follow confidence thresholds. Every decision is logged (autopilot_decisions) and the job ends with a report (outcome,rounds,residuals,reason), available atGET /api/projects/{id}/autopilot. Seedocs/autopilot.md. - Translated results returned by the API.
/api/v1/translation-requestsonly took JSON and never returned an EPUB, and a request could stay “running” or “not ready” forever. It now also takes an EPUB (multipart orapplication/epub+zipbody) or one or more.txtfiles, and returns the translated EPUB for an EPUB input — validated with EPUBCheck and repaired automatically (offending documents back to source, re-validated, at mostDELIVERY_REPAIR_ATTEMPTStimes; defects inherited from the original are reported, not blocking) — or JSON, text or a ZIP of chapters withreport.json.?wait=holds a bounded long poll; a signed webhook (HMAC-SHA256, allow-listed hosts, SSRF protection, bounded retries) announces the end. Requests always endcompleted,completed_with_residuals,failed(with a reason) orcancelled(API_REQUEST_STALL_MINUTES,API_REQUEST_MAX_HOURS), and the completion report (passage states, residual passages with their reason, costs, durations, import and autopilot decisions) comes with the status, the result and the webhook. Seedocs/api.md. - The autopilot in the interface. A book running on its own showed nothing of what the autopilot was doing, and the result still had to be looked for under Export. The workspace now shows a live line above the tabs: current phase (recovery, consistency, final review, AI arbitration), convergence round (“Round 2 of 3”) and the fallback provider used after an outage, with “nothing is expected from you”. As soon as the run ends it offers Download the EPUB (or the chapters of a text volume) and See the report. A new Autopilot tab holds the final report (rounds, passages kept in the original with their reason, logged decisions), the residual passages with links to open them or filter their decisions, and the paginated decision log filterable by stage and passage.
- Autopilot and webhook settings from the interface. The installation’s autopilot and webhook values were environment variables only, changed by editing
.envand restarting. Administrators now edit them in Settings › Autopilot (default switch, convergence rounds, outage retries and wait, ordered fallback providers, thresholds of the automatic decisions) and in Settings › Automation API (allowed hosts and private networks, attempts, timeout, global signing secret). Saved values overrideAUTOPILOT_*andAPI_WEBHOOK_*at the next decision without a restart, and can be reset to the environment. The API isGET/PUT/DELETE /api/settings/autopilotand/api/settings/webhooks(admin only, errors in French and English); the global secret is write-only, stored encrypted withSECRET_KEYand never returned. A book’s own settings now include its autopilot choice and its ordered fallback providers. - Per-token webhook signing secret in the interface. Creating an API token can also create its own webhook signing secret, shown once next to the token; tokens that have one carry a “Signed webhooks” badge.
- Markdown, HTML and DOCX chapters. Chapters written in these formats had to be converted by hand. The import assistant now takes them (one file per chapter): headings, paragraphs, list items and quotes are translated, code, tables and separators kept as they are, and the block markup is restored in text exports. DOCX and HTML are read with the same archive and XML protections as EPUB files.
- Provider comparison. Choosing a provider for a book meant guessing.
python -m app.maintenance.compare_providers --project <id> --providers <id>,<id>translates the same sample of a book with each provider, with the context the pipeline would build, and reports translated and failed passages, seconds per passage, tokens, cost and the automatic checks’ findings, with the translations side by side in JSON. Nothing is written to the book; the calls are real and billed. - Daily usage aggregates. Statistics re-read every model request, and deleting old requests would have falsified them. Tokens, costs and requests are now aggregated per day (
usage_daily);RETENTION_REQUEST_ROWS_DAYScan then delete old request rows without changing the statistics. - A library of series. Libris only knew isolated EPUB books with a free-text series name compared as a string. Series are now real records (unique name per owner, case and spacing folded) holding numbered volumes or the continuous chapter flow of a webnovel; a book without a series is a standalone volume. The library shows series first — volumes, chapters, formats, aggregated progress, activity, pending issues, provider and model, memory state, missing or duplicate volume numbers — then standalone volumes, then archives. Each series has its own page (
#series/<id>): dashboard, volumes in reading order with renumbering, attaching and detaching, webnovel chapters with their progress, import, Series Bible, series glossary with the volumes’ overrides, identities, relations, OpenViking memory, default settings, archiving and the decision log. A volume still opens in the translation workspace;#project/<id>links keep working. - Guided two-phase import (“Add content”). Importing meant picking EPUBs that became projects at once, with the series typed afterwards. The assistant now walks through format, destination, files and an editable pre-analysis before anything is created: files wait in a staging area while the server inspects them (format, title, author, language, proposed series, volume or chapter number with its confidence and reason, hash, duplicates, warnings). Volume numbers come from your correction, then the consensus of the whole batch of file names, then the file name (
Vol. 2,Volume 2,Tome IV,Book 3,v03,#04,[05]), then the EPUB series metadata. An EPUB goes to a series or an explicit Standalone volume; numbers used twice or already taken are resolved, gaps are shown. The summary starts the whole pipeline by default (“import only” and “import and analyze” remain). Confirming twice imports once. The assistant is usable on phones. - TXT chapters for webnovels. One file per chapter, UTF-8 with or without BOM, UTF-16 with BOM, or — flagged — Windows-1252; only the encoding, line endings and forbidden control characters are normalized, and blank lines, indentation and scene breaks are kept. Chapter numbers come from names such as
Chapter 001 - Beginning.txt,Chapitre 12.txt,Ch. 12.txt,C012.txtor001.txt, in natural order. Chapters go to the series’ continuous flow, an existing volume or a new one, and can be added later without retranslating the others. A different content for an existing chapter needs an explicit replacement, which keeps passages whose text did not change (human corrections included), asks before dropping human work and marks later chapters for a new check. - Automation API. Scripts had to drive the interface with a session cookie.
/api/v1accepts only API tokens (shown once, stored hashed, scopesseries:read,content:write,pipeline:start,jobs:read,jobs:control,results:read, optional expiry, revocation, audited, managed from the account and settings pages).POST /api/v1/translation-requeststakes a strict JSON document (or the same as a.jsonfile) with series, volume and chapters, honoursIdempotency-Keyand external identifiers, stores everything in SQL and answers202 Accepted; the worker starts the pipeline when the volume is free, across restarts. Status, pause, resume, cancel and results as JSON, consolidated text or a ZIP of chapters with checksums are available. Seedocs/api.md. - Series memory. Characters of each volume are linked to series identities (an ambiguous name stays a proposal; merges and splits are decided by a person and audited); places, organizations, objects, relations and accepted terms are aggregated into a Series Bible and a series glossary. Prompts receive the identities met in earlier volumes, under the names those volumes used, and the series terms: explicit instructions, human decisions, locked volume terms (or an audited volume override), locked series terms, accepted series terms, then automatic proposals.
- Exports per format. A TXT or JSON volume exports one UTF-8 file per chapter in a ZIP with a checksum manifest, a consolidated text with chapter headings, or Markdown;
POST /api/exports/textexports several volumes as one ZIP. The export menu only offers what the volume’s source allows.
Changed
- Cheaper prompts. Sections of the user prompt are now ordered from the most stable (book context) to the most variable (the passage), so providers with a prompt-prefix cache reuse about 2,400 tokens per call: −24 % of uncached input per passage at high quality on the synthetic provider (
scripts/measure_prompt_cost.py).REVIEW_MODE=fusedreviews and corrects a passage in one call at high and maximum quality, falling back to two calls when the window is too small;PASSAGE_MAX_CHARS(or a volume’s setting) sets the size of passages. - No confirmation left in the import assistant. An uncertain volume or chapter number blocked the import until someone confirmed it. The number is now applied automatically, marked “chosen automatically; you can change it here”, a volume without a number gets the next one of the series, and the end screen lists these automatic decisions with their reason. Import and run the whole pipeline is the main button and hands each volume to the autopilot. Confirmations come back only when the server requires them (
IMPORT_CONFIRM_LOW_CONFIDENCE=true). - The validation queue becomes a review log. The “Validations” tab presented passages “to review” as if the export waited for them. It is now the Review log: remarks, doubts and alerts with the AI’s decisions about them, passages “open to optional review”, and the progress and library say “open” instead of “to review”. Human correction, validation and accepting or rejecting a suggestion all remain available; none is required.
- Text export of EPUB books. The TXT export glued every passage together; it now follows chapters, each under its translated heading, without the table of contents or attributes, and Markdown puts a
##heading above each chapter. - Project archives version 3 carry every source file of a volume, its series and the new chapter fields, and restore TXT and JSON volumes with the same unit identifiers; entries, sizes, compression ratio and checksums are checked before anything is written.
- OpenViking per series. A volume of a series now lives in
<root>/<owner>/series/<series>/volumes/<volume>, a standalone volume in<root>/<owner>/standalone/<volume>. A passage searches its series space but only keeps events SQL admits — earlier passages of its volume and earlier volumes of its series, never a later chapter or volume, never a document that differs from its SQL event. Event documents carry canonical metadata (owner, series, volume and number, chapter and position, memory type, identities, validation, creation date) and are derived from SQL alone. Series pages show the backlog and offer resync, rebuild from SQL and reindex. Internal memory reads earlier volumes the same way. - Prompt files
file-v3. The analysis and translation prompts mention the identities known from earlier volumes. - Job leases on the database clock. A worker whose clock drifted could take over a job still held by another; leases are now written and compared with the database’s time.
- Hardened deployment. The production Compose file is versioned in the repository; containers drop capabilities, forbid privilege escalation and run read-only where possible; Python dependencies are pinned with hashes;
deploy/libris-runner-prune(and its timer) removes the orphaned volumes CI jobs leave on the runner. The JSON-vs-JSONB study is documented in the architecture guide (JSON kept, converted column by column when a query needs it).
Fixed
- OpenViking memory emptied after a week. Remote events were named after send-queue rows that the retention deletes after seven days, and retrieval only admitted events that still had such a row: in 0.5 the remote memory of any book older than a week was silently ignored, and a rebuild could only replay what was left. Admission and rebuilds now rely on the SQL memories.
- SQLite migrations. Table rebuilds ran with foreign keys enforced: rebuilding a parent table cascaded to its children (a downgrade touching
projectsorsegmentscould delete chapters or translation versions). Migrations now run with foreign keys off under SQLite. - Flaky deployment test. The fake
dockerof the deployment tests rewrote its whole state on every call; a slow runner lost a removal and failed the pipeline intermittently.
Security
- Automation tokens are stored as SHA-256 hashes, compared in constant time, scoped, rate-limited per token, never logged or returned after creation;
/api/v1refuses session cookies and the interface API refuses tokens. Request bodies with a token are bounded byAPI_MAX_PAYLOAD_MBbefore being read. - Imported files are never located by the name they were uploaded with: sources and staged uploads live under
DATA_DIRat paths Libris chooses, archive entries are fixed names, and no URL is ever fetched from a payload. - Webhooks are only sent to allow-listed hosts, never to private or loopback addresses unless explicitly allowed, without redirects or environment proxies, signed with a per-token or global secret that is write-only and stored encrypted.
[0.5.0] - 2026-09-18
A new interface and the remaining findings of the September audit: lower model costs, several passages of a book translated at once, a faithful project archive, scheduled backups and a simpler release pipeline.
Upgrading. Five database migrations run automatically at start-up (alembic upgrade head in the Docker deployment); on a large production database they take a minute or two, mostly to fill the translation-memory key of existing passages. Behaviour changes to know about:
- The worker now purges diagnostic data every hour: prompts and raw answers of model requests older than 30 days (the requests, their tokens and costs stay), progress events older than 7 days except the last 500 per book, sent memory-outbox rows, automatic Book Bible revisions beyond 20 per book, and the per-passage state of jobs finished more than 30 days ago. Every rule has a
RETENTION_*setting;0disables it. To give the freed space back to the system, runVACUUM (FULL, ANALYZE) llm_requests;once with the worker stopped. - A provider now serves several passages of the same book at once, up to its
max_concurrency; setWORKER_BOOK_PARALLELISM=1to keep one passage at a time. - Identical passages already translated in your books are reused without a model call (translation memory, on by default per book).
/openapi.jsonrequires a session;GET /metricsstays disabled untilMETRICS_TOKENis set.- Uploads are refused before being read when they exceed
MAX_UPLOAD_MB(or 1 MiB without a session). - Behind a reverse proxy, keep
FORWARDED_ALLOW_IPSset (see 0.4.1).
Security
- Resource exhaustion by a signed-in account. Live progress streams were unbounded and queried the database inside the API’s event loop; an EPUB made of many small, highly compressed files passed the compression check (319 KiB unpacked to 285 MiB); every chapter preview rebuilt and unpacked the whole book. Live streams are now limited per account (
EVENT_STREAMS_PER_USER, 4) and per process (EVENT_STREAMS_TOTAL, 100) and poll the database from a worker thread; the declared unpacked size and the whole-archive compression ratio (MAX_COMPRESSION_RATIO, 100) are checked before an EPUB is unpacked; previews reuse a bounded cache (PREVIEW_CACHE_MB, 64) that any edit invalidates (#52). - API description and provider addresses.
/openapi.jsonwas public; it now requires a session and can be removed withOPENAPI_ENABLED=false. Non-administrators no longer see providers’ addresses, only their name and model. Changing the address or type of a provider that holds an API key now requires entering the key again, so a stored key cannot be redirected to another host (#52). - Web search client. The SearXNG client followed the server’s proxy environment variables and read answers of any size; it now ignores them, like every other outbound client, and stops reading beyond 2 MiB (#52).
- Series conventions. An editor of a shared book could give it the name of one of the owner’s private series and pull that series’ terms and translation decisions into the book’s prompts. Only the owner may now attach a book to a series that contains books the editor cannot read (#52).
Added
- Prometheus metrics. Nothing told an operator from outside that the worker had stopped taking jobs, that a job’s lease had expired, or how many tokens each step of the pipeline burns: the cost analysis of the production instance had to be done by hand in SQL.
GET /metricsnow exposes, in the Prometheus text format, jobs by operation and state, the age of the oldest job waiting for a worker, expired leases, model requests by operation and outcome, input, output and wasted tokens by operation, cache hits, requests in flight per provider (its name, never its address), passages by state and the external-memory backlog. Counters are read from the database, so they are cumulative and survive restarts; no label carries a book title, text or URL. The endpoint is disabled (404) untilMETRICS_TOKENis set (24 characters at least), then requires it as a Bearer token; results are cached for 10 seconds. The operations guide has a scrape configuration and alert examples (#61). - Cost estimate before starting a job. A book costs tens of millions of input tokens, and the first figure came after the work was done.
GET /api/projects/{id}/estimate?operation=analyze|translate|reviewreturns the passages still to process, the expected requests, input and output tokens and the cost at the provider’s current prices, without calling any model. Passages already analysed, finished, corrected by hand or validated are not counted. The figures come from the owner’s previous books on the same provider when at least 5 passages were processed (retries and errors included), otherwise from documented defaults;basissays which (#61). - Wasted tokens. The cost of a book counted input tokens spent on requests that failed validation, were refused or interrupted as if they were useful work — about 12 % of the production spending.
GET /api/projects/{id}/metricsand, per model,GET /api/statistics/modelsnow also reportwasted_input_tokensandwasted_share; existing fields are unchanged (#61). - Glossary exchange with translation tools. Glossaries could only be exchanged as Libris JSON or as a comma-separated CSV with English column names, so a termbase from a CAT tool or a CSV saved by a French spreadsheet could not be imported. The glossary now also exports to TBX (TBX v3 with TBX-Basic data categories: subject field, definition, one language section per book language; locked, accepted and proposed terms travel as the standard
preferred,admittedanddeprecatedstatuses), and the import recognises JSON, CSV or TBX by extension or content. TBX v3 and older TBX v2 (martif) termbases are read with the same safe XML reader as EPUB files. CSV files may use;,,or tabs, UTF-8 with or without BOM, UTF-16 or Windows-1252, common French or English headers (“Terme source;Traduction”, “Source term”, “Target”…), yes/no values in both languages, or no header at all; the quote added on export to protect terms such as “-kun” from spreadsheet formulas is removed again. An invalid file is refused with the line or term at fault (#52). - Error messages in English. With the interface in English, the API still answered its errors in French. Error messages are now translated when the request’s
Accept-Languageprefers English, including messages that carry a value (book titles, counts, HTTP statuses), input validation messages and EPUBCheck report summaries; French remains the default. A test fails whenever a message raised in the code has no English version (#52). - Several passages of a book at once. A book was translated one passage at a time, so a provider able to serve four requests worked at a quarter of its capacity whenever a single book was running. Translation, the final review and the consistency checks now keep several passages of a book in flight, up to the provider’s
max_concurrency, shared fairly between the books using it at the same time; model calls wait their turn per provider in arrival order. With a fixed-latency mock and a capacity of 4, a book goes from 3.7 to 13.2 passages per second in translation and from 3.7 to 14.0 in final review. A passage’s context shows the translation of the neighbours already done and only the source of those still in flight; chapter analysis and the Book Bible synthesis stay sequential because each step builds on the previous one. A pause, a cancellation or an outage stops every call in flight, and a resumed job redoes only the interrupted passages. The newWORKER_BOOK_PARALLELISMsetting caps the number per book (0, the default, follows the provider;1restores one passage at a time) (#51). - Translation memory: every passage cost a model call even when you had already translated exactly the same text (the title repeated in each document, identical interludes, recurring passages across a series). A passage whose source is identical (Unicode forms and spacing aside, formatting included) to a finished passage in one of your books with the same language pair now reuses that translation without a model call, preferring your validated versions; in a series only the same book and earlier volumes are used. The version is labelled
translation_memory, and review and final review still apply. The Translation memory setting (on by default,translation_memoryin the project settings API) and the number of reused passages appear in the book strategy panel. On a test book with five identical interludes, 14 of 22 passages were reused. (#54) - Right-to-left export: a book translated into Arabic, Hebrew, Persian or Urdu kept a left-to-right layout. Exported documents now get
dir="rtl", EPUB 3 spines turn pages right to left, contrary inheriteddirattributes are adapted, and elements that declared the source language no longer announce it over the translated text (quotations in a third language keep theirs). (#54) - Book blurb translated:
dc:description(and shortdc:subjectentries) stayed in the source language in the exported book. They now form a “Book metadata” section, translated like any passage. (#54) - Unsaved translations are protected. Switching tab, section, page or book, or closing the browser tab, with an edited but unsaved passage used to discard it silently. Libris now asks before leaving, and text typed while a save is in flight stays as a draft instead of being overwritten.
Ctrl/⌘+Ssaves the passage andCtrl/⌘+Entervalidates it. (#50) - Cost estimate before paid operations. Starting an analysis, a translation or the AI review happened on a single click with no idea of its cost. A confirmation now shows the server’s token and cost estimate when it can provide one. (#50)
- Screens for existing features. Project members can now be listed and revoked, a provider can be deleted (the server still refuses while it is in use and explains why), per-passage guidance can be edited, the book memory can be reindexed or rebuilt, the translation memory setting can be switched per book, and the glossary imports and exports JSON, CSV and TBX. Characters are edited in a form instead of raw JSON. (#50)
- Full journey test. A
@journeyPlaywright spec imports an EPUB, registers the mock provider, lets analysis and translation run on the real worker, validates a passage and exports the EPUB. (#50) - Rollback. When a release migrated fine but misbehaved, there was no quick way back: the deployment procedure refused any older version and the guide said to rebuild the previous tag. A manual, protected
rollback-productionjob now redeploys the previous version, whose images every deployment keeps on the host, through the same guarded procedure (dump, health check, worker restart from its checkpoints). It refuses before touching anything when the database schema changed since that version, and points to the dump restoration instead (libris-production-deploy --rollback, see Rollback in the release guide) (#56). - Scheduled backups. The only backups were the pre-deployment dumps, kept on the production disk and without the books: losing that disk lost every book and translation.
deploy/libris-backupand its daily systemd timer dump the database while Libris keeps running, archive the books volume, read both back before the backup counts, write checksums to a share of another host, refuse to write when that share is not mounted, and rotate old backups only after a verified one.deploy/libris-restorerestores a backup into a separate throwaway Compose project (never the worker) and checks migrations and health, for a monthly restore test;--dry-runchecks a backup without Docker. Seedocs/backup.md; nothing is installed automatically (#56). - User journey in CI. No automated test ever clicked through import, analysis, translation, validation and export against a real backend. The
e2ejob starts the real Compose stack with the synthetic model ofdocker-compose.test.ymland runs the Playwright specs tagged@journey; the report, traces, screenshots and service logs are kept when it fails (#56). - EPUBCheck in the tests. The test suite never ran the EPUB validator that the image ships, so an export EPUBCheck rejects was only discovered by users. Tests marked
epubchecknow export the reference book, an EPUB 2 with entities and a translated table of contents and validate them with the real EPUBCheck 5.3.0 inside the built image (skipped elsewhere unlessEPUBCHECK_JARis set) (#56).
Changed
- Lighter project list. The library polls the project list every five seconds, and each book cost 12 to 16 database queries plus its whole Book Bible: about 700 queries per tick and per open tab for 50 books. The list is now computed in a constant number of queries whatever the number of books (8 books: 99 queries before, 10 now) and no longer includes the Book Bible, which the book’s own page still loads; every other field keeps its value (#52).
- Job checkpoints (migration). Each job kept, in its checkpoint, a list of every passage it had finished or reviewed — with the outcome of each review and the repaired batches —, rewritten at every step and sent back with every job listing: up to 443 KB per job in production, and several megabytes on a very long book. That state now lives in a dedicated table, and a checkpoint only holds a cursor and counters (a few hundred bytes, whatever the book). The migration converts existing jobs in place: a paused job resumes without retranslating anything and past review results keep showing;
alembic downgraderestores the former format. Project archives carry this state with their jobs, and an archive exported earlier is converted when it is restored (#51). - Data retention: state of finished jobs. The per-passage state of a job (see Job checkpoints) would have been kept forever. Once a job has been completed, failed or cancelled for more than 30 days (
RETENTION_JOB_STATE_DAYS,0disables), the worker’s hourly clean-up deletes what only served to resume it and keeps the final review outcomes shown in the book’s review history; paused or waiting jobs are never touched.python -m app.maintenance.retention --dry-runreports it (#51). - Prompts resist text that pretends to be instructions: a book quoting
</TARGET_TEXT>could close a prompt section and speak with the prompt’s authority. Section contents are now escaped, every prompt (administrator overrides included) states that book text and context are untrusted data, languages are named (“French (fr)”) instead of raw codes, writing and review prompts carry a tu/vous consistency rule and the target language’s typography (French no-break spaces, « », dialogue dashes), the marker instruction matches what validation accepts, revision and final review no longer disagree on uncertainties, and the JSON schema is sent once instead of twice. Prompt versions (file-v2,db-vN,+rules-v1) show the change in the inspector. (#54) - Small context windows: with an 8k or 16k provider every passage failed with advice to shorten the instructions, whatever they were. The budget now reserves the actual response schema instead of a fixed 6,000 tokens, the error gives the real figures (window, output reservation, prompt, passage, rules) and the window that would fit, and on a small window a long passage is translated in parts cut at sentence boundaries, then reassembled. The context inspector’s input estimate is now exactly what is checked against the window. (#54)
- Redesigned interface. The navy and coral interface mixed fonts, boxed every element and gave every button the same weight. Libris now has a collapsible sidebar (a drawer on phones), a warm light theme and a neutral dark theme that follow the system by default, one indigo accent, Inter for the interface and a book serif for the text being translated, reusable components and design tokens. The library offers a segmented filter, a dense table or card view and drag-and-drop import; a book shows one contextual main action, a slim five-step stepper and tabs; the review queue shows source, translation, AI doubts and proposals side by side with Accept, Edit and Reject. (#50)
- Formatting codes are no longer shown raw. The editor printed
⟦t0⟧…⟦/t0⟧codes as-is. The source text now shows the book’s formatting, and in the translation field the codes appear as discreet chips; the text sent to the server is unchanged, and a warning appears when the codes no longer match the source. (#50) - Paginated review queue. The validation screen loaded and rendered every flagged passage at once; it now loads twenty at a time. (#50)
- Localised interface text. Plurals follow each language’s rules instead of “(s)”, numbers, percentages and dates follow the interface language, a few remaining hard-coded French strings went through the translation catalogue, conflicting English translations of the same text were unified, and every API request sends
Accept-Languageso server errors come back in the interface language. (#50) - Accessibility. Native
prompt(),confirm()andalert()dialogs are replaced by accessible dialogs with focus management; links of the character graph are listed as focusable buttons and announced to screen readers; focus is visible everywhere, phone touch targets are at least 40 px and animations respect reduced motion. (#50) - Faster, leaner pipeline. Release tags re-ran every test already passed on the default branch, pip and npm downloaded everything at every job, and a documentation-only merge request tested and built like a code change. Tags now promote the images the default branch verified, without re-testing (about 2 minutes less before the deployment starts); dependencies are cached per lockfile; the frontend job installs once; Trivy analyses each image once; a documentation-only merge request only runs the version, dependency and secret checks. Playwright specs are selected by tag (
@integration,@journey), so the ten mocked specs all run instead of a hand-maintained list of eight (#56). - Release notes. The GitLab release showed the whole CHANGELOG since 0.1.0, and re-running the release job failed because the release already existed. Releases now contain only the section of the tagged version, the tag pipeline fails early when that section is missing, and a re-run updates the release (#56).
- Production disk. Every deployment left its application and Codex images behind on the production host (4.7 GB found). A successful deployment now removes the Libris images that are neither deployed nor kept for rollback, and the untagged pulls of the Libris registry; no other image or volume is touched.
libris-production-deploy --prune-images --dry-runshows what would go (#56). - Migration checks. Downgrades were only tested down to one intermediate revision, and GitLab never compared the models with the migrations. The CI now migrates PostgreSQL up, all the way down to an empty schema, up again, and run
alembic check; the SQLite test does the full round trip too (#56).
Fixed
- Release pipeline. The production deployment only waited for the release images to be tagged, so it started while the container runtime test and the vulnerability scan were still running: a failed gate could not stop it. It now waits for the runtime test, the scan and the GitLab publication, and a test keeps those gates in place. The release guides describe the deployment as automatic, consistently (#30).
- Locked glossary. A correct translation was rejected when it differed from the locked term only by typography or grammar — a curly or straight apostrophe, a no-break space, unaccented capitals, a simple plural, or a contracted article (“du Conseil” for “le Conseil”) — and a capitalised term such as “Will” was triggered by the ordinary word “will”. Each false alarm cost up to thirteen model calls and left the passage in error. The check now ignores those differences, still reports a term that is really missing, and its message names the expected terms (#31).
- Invalid model answers. An answer refused by validation (missing or merged paragraphs, invalid JSON, locked glossary) was asked again unchanged, up to five times at full price. The next attempt now tells the model why its answer was rejected, and validation failures stop after three attempts so that the cheaper small-batch repair takes over; network errors keep their five attempts (#32).
- Context allocation. The two passages before and after the one being translated took the whole optional context allowance on their own, so validated character sheets, accepted glossary terms and the chapter state were silently left out of every request, whatever the model’s window. The neighbourhood now gets at most 60 % of that allowance when other material competes for it, and a neighbour that is too long is shortened to an excerpt instead of being dropped (which could end in a misleading “window too small” error). The default allowance, hence the cost of a request, is unchanged; the Context Inspector’s input estimate is now correct (#33).
- EPUB import. Two kinds of ordinary books could not be imported. EPUB 2 files using named HTML entities (
,é,……) were refused with “Entité XML non résolue”: these entities are now converted from their fixed table, while entity declarations and external DTDs remain refused. A manifest entry whose file is missing from the archive (a deleted font or image) caused an HTTP 500: title, author and language are now read from the package already parsed, the book imports, and the dangling entry is dropped from the exported EPUB; a missing document of the reading order is refused with its file name (#34). - Table of contents. A translation — typed by hand or returned by the model — could add text next to the link of a contents entry (
<li><a>…</a> extra</li>). Nothing objected until the final export, which then failed with a raw EPUBCheck message (RSC-005) that did not point to the passage. Such a translation is now refused when it is saved, with an explicit message, and the model is told why. Page lists (page-list, NCXpageList) are no longer split into passages, so newly imported books stop paying model calls to “translate” page numbers; books already imported are unaffected (#35). - Resuming a job. Every time a job resumed — after a pause, a worker restart or a provider outage — it rewrote its checkpoint and emitted a progress event for each passage already done, at every stage: thousands of database transactions and events per resume on a long book (one production book reached 86,000 events). Passages already settled are now set aside in one query before the loop, so a resume only writes for the work that remains; progress numbering still spans the whole book (#36).
- Worker heartbeat. A single database error while renewing a job’s lease — a brief PostgreSQL restart, a saturated connection pool — cancelled the model call in progress, often a long one already paid for, which was then made again. The lease lasts 60 seconds, so the heartbeat now keeps trying and only gives up after 40 seconds without a successful renewal; a pause, a cancellation or a lost lease still stop the job at once (#37).
- Database indexes. Several queries that run in a loop had no suitable index: the count of a provider’s running calls taken before every model call, the look-up of a passage’s analysis memory, the event feed polled by each open browser tab, the memory outbox scan, and the foreign keys followed when a book is deleted. A migration adds them (
alembic upgrade head, applied automatically by the Docker deployment; it takes a few seconds even on a large database) (#38). - Upload size (security). File uploads were written to the server’s temporary directory in full before the session was checked and before
MAX_UPLOAD_MBwas applied: anyone able to reach Libris could fill the disk without an account. Requests are now bounded before anything is buffered — 1 MiB without a session cookie (answered401),MAX_UPLOAD_MBplus a small margin with one (answered413) — fromContent-Lengthwhen it is declared, and while reading for chunked transfers (#39). - Exports without the source file. When a book’s original EPUB could not be found on the server — a data directory restored under another path, a deleted file — every export failed with an HTTP 500, including plain text, Markdown and the Book Bible, which do not need it. The file is now also looked up at its deterministic place under
DATA_DIR/books, the text exports and the Book Bible keep working without it, and the EPUB, project archive and preview answer with an explicit message (#40). - Documentation. The installation guides still pinned version 0.3.1; they now follow the current release and
scripts/check_version.pyrefuses a release whose guides pin another version. A “JSON” export was advertised that does not exist (the JSON export is the Book Bible), the native Anthropic and OpenAI providers were missing from the README, the provider retry delays (60 s to 1 h) and the worker heartbeat (2 s) were misstated, six settings were undocumented (SESSION_DURATION_HOURS,FORWARDED_ALLOW_IPS,WORKER_HEARTBEAT_SECONDS,MEMORY_CATALOG_INTERVAL_SECONDS,PROVIDER_RECOVERY_BASE_SECONDS,PROVIDER_RECOVERY_MAX_SECONDS), and.env.examplenow warns thatCOOKIE_SECURE=truebreaks logins over plain HTTP (#41). - Data retention (behaviour change). Nothing was ever purged: on the production instance the request log had reached 8.3 GB of a 8.9 GB database, with 612,000 progress events. The worker now cleans up at start-up and every hour: request logs older than 30 days lose their prompt, raw response and context trace — the row, its token counts, cost, duration, error and cached answer are kept, so statistics and the request cache are unaffected —, progress events older than 7 days are deleted except the last 500 of each book, as are memory-outbox rows sent more than 7 days ago and automatic Book Bible revisions beyond the last 20 per book (human revisions are all kept). Each rule has a setting (
RETENTION_REQUEST_BODIES_DAYS,RETENTION_EVENTS_DAYS,RETENTION_OUTBOX_SENT_DAYS,RETENTION_BIBLE_REVISIONS;0disables it) andpython -m app.maintenance.retention --dry-runreports what would go. A cache hit no longer stores a second copy of the whole prompt. See the operations guide for reclaiming disk space (#42). - Jobs without a provider. A job whose provider had been deleted, or that was created before any provider was chosen, stayed “pending” forever without a word, and kept the book locked against any other work. It is now marked as blocked with an explicit message — choose a provider for the book, then resume the job — and resuming starts it normally (#43).
- Summary & recovery. The completion report loaded every passage of the book — units, translations, critiques — just to count them, and it is refreshed with every burst of job events: about 1 MB and 0.2 s per call on a 2,300-passage book. The counts now come from the database, and the list of passages to recover reads only the columns it shows and is limited to the first 500, with the total displayed when there are more (#44).
- Cost statistics. The cost of a book was recomputed with the provider’s current prices, so changing a price — or fixing a typo in one — silently rewrote the cost of every book already translated; requests of a deleted provider were left out, and the per-model statistics attributed a provider’s whole history to its current model. Each request now records the prices in force (migration adding two columns, instantaneous), the cost uses them and only falls back to the current price for older requests, requests of a deleted provider are counted, and the per-model statistics use the model recorded with each request (#45).
- XML entity declarations (security). Libris refuses EPUB files that declare XML entities, but it looked for the declaration as ASCII bytes: a file encoded in UTF-16 went through, and entities used inside an attribute value were expanded. The check is now made on the parsed document type, whatever the encoding. External entities and network access were, and remain, disabled (#46).
- Changed or lost
SECRET_KEY. Provider API keys are encrypted withSECRET_KEY. After a restart with another key — a database restored without its.env, a rotation — every model call failed as an “invalid answer”, was retried, and passages ended in error without any explanation; the provider test showedInvalidToken. Libris now detects an unreadable stored key before calling the provider, puts the job on hold like an authentication failure, and tells you to enter the key again. An unreadable OpenViking key only disables the external memory, with a warning in the logs (#47). - Memory relevance. Memories recalled for a passage were ranked by the words they share with the retrieval query — which begins with an English instruction meant for the semantic search service. Words such as “relationships”, “objects” or “known” therefore favoured the same memories whatever the passage said. The local ranking now only compares with what comes from the book (entities, neighbouring text, the passage, specific needs); the query sent to an external memory service is unchanged (#48).
- EPUBCheck load. Every import and export starts a Java process to validate the EPUB, for up to 90 seconds, with no memory ceiling and no limit on how many run at once: a few simultaneous exports could take the host’s memory and processors away from the API and the worker. At most two validations now run at a time per process (
EPUBCHECK_CONCURRENCY), each capped at 1 GB of heap (EPUBCHECK_MAX_HEAP_MB); the others wait their turn, and a saturated server answers with an explicit “try again” message (#49). - Project archive. Restoring a project archive lost the work it was meant to protect: validated passages came back “to check”, critiques, uncertainties, quality issues, jobs, Book Bible revisions and request statistics were gone, the version history was doubled, and an archive without its project section answered HTTP 500. Archives now use format version 2, which carries the whole state of the book and is validated before anything is written: an incomplete or altered archive is refused with the faulty fields named, never with a 500. Owners, members, permissions and the provider are deliberately not restored — the person restoring becomes the owner and chooses a provider — and a job that was running comes back paused. Version 1 archives remain readable. The export now refuses an archive that the import could not read back (
MAX_UPLOAD_MB,MAX_UNPACKED_MB) and says which setting to raise, and a restore no longer blocks the API while it runs (#52). - A large book slowing down the others. The worker ran its database queries and book-sized computations (context building and memory scoring, consistency sampling, logging of model calls) directly in the loop shared by all its jobs: with two books of 1,500 passages, the loop stalled for up to 450 ms at every passage, delaying the lease renewals of every other book. That work now runs in background threads, lease renewals have their own, and memory scoring no longer re-reads the query for every memory: the worst stall drops to about 100 ms and leases are renewed on time (#51).
- Edits while the worker writes (PostgreSQL). Saving a human correction, or keeping the original of a passage, while a job was writing that same passage could end in a database deadlock, and one of the two operations failed with an error. The API now takes its locks in the same order as the worker, so it waits a moment and, if the worker’s version arrived first, reports the usual “modified meanwhile” conflict (#51).
- Series: an earlier volume’s locked term no longer changes in a later one: an unlocked term of volume 2 overrode a term locked in volume 1, locked series terms were never checked in the output, and “en”/“en-US” or “Saga”/“saga” silently separated volumes. Locked series terms now win and are checked like the book’s own locked glossary, series and languages are matched after normalisation, and human corrections validated in earlier volumes reach later ones as short before/after choices instead of whole passages. Nothing from later volumes is ever used. (#54)
- Long Japanese, Chinese and Korean paragraphs are split: a 9,000-character paragraph was never cut because sentence boundaries required a following space, and exceeded the model’s output. Paragraphs are now cut after 。!?… (closing quotes included). (#54)
- Furigana stay furigana: ruby readings (
rt,rp) were translated along with the base text; they are now kept as they are. (#54) - No more text lost or scattered at import: text mixed with block elements (“Dear Alice, … signature”) became one fragment per text node, and text following a comment outside a paragraph was dropped. Each run of text between blocks is now one passage unit; SVG text and
aria-labelare translated; preformatted text, formulas and SVG titles are kept and listed in the import report instead of vanishing silently. (#54) - Section count excludes the table of contents:
nav.xhtmlandtoc.ncxwere counted as chapters (8 sections for a 6-chapter book); their passages are still translated and shown. Pages markedlinear="no"now come after the story instead of opening it. (#54) - “Accept all” applied every AI proposal on a single click. It now asks for confirmation first. (#50)
- Misleading empty states. Screens showed “no books” or “nothing to review” while their data was still loading; they now show placeholders until the answer arrives. (#50)
- Batch series numbering ignored the interface language when sorting titles. (#50)
- CI jobs leaving containers behind. Cancelling a stuck job killed only the runner’s
dockerclient: the anonymous test container went on running (24 minutes seen), its throwaway PostgreSQL stayed up and nothing cleaned them. Every CI container is now named and labelled after its job and removed by the job’safter_script, including on cancellation; test commands and jobs have explicit time limits so a hang fails within minutes, and containers older than two hours are reaped by the next pipeline (#56). - Release tag racing its own branch pipeline. A tag pushed right after its merge could start before the default-branch pipeline had built, or finished testing, that commit’s images, and failed on a missing image — or promoted an image whose checks were still running. The branch pipeline now marks a commit’s images once every job has passed (
verified-sha-<commit>), and the tag pipeline waits up to 20 minutes for that mark before promoting them (#56).
[0.4.1] - 2026-09-18
Follow-up to 0.4.0: the remaining findings of the audit. No database migration and no configuration change is required; behind a reverse proxy, consider setting FORWARDED_ALLOW_IPS (see Security).
Security
- Login throttling. The brute-force protection counted every login — successful ones included — in a single bucket per client address. Behind a reverse proxy all visitors share the proxy’s address, so twenty logins in five minutes locked everybody out, and anyone could do it on purpose. Only failed attempts now count, per client address and account name (with a higher overall ceiling per address), a successful login clears the counter, and expired entries are purged. Set
FORWARDED_ALLOW_IPSto your proxy’s address so that real client addresses are used (#22).
Fixed
- Edit conflicts in the editor. When a running job delivered a new version of a passage while you were typing, every save was refused (HTTP 409) and nothing let you get out of it short of reloading the page. The notice now offers two explicit choices: reload the server version, or keep your text and save it over the latest version (#23).
- No more out-of-date answers on screen. Switching chapter, filter or page quickly while the server was slow could show the previous chapter’s passages under the new chapter title, and overlapping refreshes could leave the book header in a past state. Only the most recent request now updates the screen. The completion report also follows the job’s progress and no longer needs a manual “Actualiser le bilan” to re-enable “Relancer la sélection” (#24).
- Data consistency. Restoring a “source kept” version (or re-importing a project archive containing one) now brings back the dedicated “Original conservé” status, so the passage stays listed among those still needing a translation; conversely, replacing a kept original with a real translation settles its standing alert. Importing a glossary whose JSON root is an object (
{"terms": [...]}) is refused with an explicit message where it used to answer “0 imported” and swallow the mistake. Saving a character sheet now refuses an empty or over-long name and a name or alias that already belongs to another confirmed character, like the dedicated alias action already did (#25). - Database migrations now run on SQLite, the default
DATABASE_URL:alembic upgrade headused to stop on the job/provider migration with “No support for ALTER of constraints in SQLite dialect”. PostgreSQL installations are unaffected (#26). WORKER_HEARTBEAT_SECONDSandMEMORY_CATALOG_INTERVAL_SECONDSare now honoured by the worker (they were declared but ignored); out-of-range values are refused at start-up (#27).- Applying accepted suggestions. The step that applies the AI suggestions you accepted kept a database connection open for the whole duration of each model call — minutes with a slow provider — which could exhaust the connection pool when several books were in that step. It now reads, calls the model, then writes, without holding anything in between; and a result that arrives after you paused or cancelled the job is no longer applied (#28).
- “Retranslate” really asks the model again. A forced retranslation of a passage whose context had not changed was silently answered from the request cache, so it returned the very same text without calling the provider. Forced jobs now bypass the cache lookup; the fresh answer is stored and reused as usual (#29).
Changed
- Operations guide:
docs/ci-cd.mdnow lists what lives outside Git on the production target and gives a verified procedure to re-provision/opt/libris-productionif it is lost, with a warning to free disk space insidebackups/and never by deleting the directory (#21). - CI: the image build job now removes this project’s own unused images older than three days from the shared runner (they remain in the registry); per-commit images had been accumulating there indefinitely (#20).
- Removed the unused “structured error codes” module and circuit-breaker helpers announced in 0.3.4: nothing ever called them and their retry tables contradicted the real behaviour, which is the one described under Automatic provider recovery in 0.4.0 (#27).
[0.4.0] - 2026-09-18
A maintenance release coming out of a full audit of the application and of its production instance. It fixes the EPUB 3 export failures, makes the worker and provider recovery robust, stops two disk-space leaks, and repairs a number of interface defects. It is a minor version because a few behaviours change on purpose: sessions now last the documented 24 hours by default (SESSION_DURATION_HOURS), the live event stream of a book no longer replays its history, starting a job on a busy book answers HTTP 409, and “Configure selection” no longer overrides languages and qualities you did not set. No database migration is included.
Fixed
- EPUB 3 export compatibility. Fully translated EPUB 3 books could not be exported (
HTTP 422, “EPUBCheck signale un EPUB invalide”) when the source file carried conversion artefacts from Kobo, Calibre or Sigil. Nine volumes of one series were affected in production while their EPUB 2 siblings exported fine, because only EPUB 2 packages were normalized before validation. Export now repairs these inherited defects deterministically, without touching the translated text, and EPUBCheck remains a blocking gate (#4):<script src>stubs whose local target is missing from the archive (typicallyjs/kobo.js) are removed (RSC-007);- the manifest
scriptedproperty is reconciled with what each content document really contains (OPF-014/OPF-015); - the NCX
dtb:uidis realigned with the package unique identifier (NCX-001), for EPUB 2 and EPUB 3; - empty XHTML
<title>elements receive the book title (RSC-005).
- Readable export and API errors. A refused EPUB export now tells you which book failed and lists the first EPUBCheck errors, in both single and bulk export; previously the interface showed only “Export refusé (HTTP 422)” or a raw JSON report. Form validation errors show the field and the reason without ever echoing the typed value back (the login screen could display the password just entered), and an HTML error page from a reverse proxy is reported as
HTTP 502/504and no longer as a JSON parsing error (#5). - Worker stability. An unexpected error in a single job could stop the whole worker and interrupt every book being translated — for example pausing a book while the provider was refusing a passage, or a provider sending a malformed
Retry-After: infheader. Each job is now isolated: the failure is recorded on that job only and the other books keep going (#6). - Automatic provider recovery. A provider answering with a tiny or fractional
Retry-After(for example0.5) made the job retry immediately in a loop, and anyRetry-Afterswitched the exponential backoff off entirely; the backoff is now a minimum wait that a provider can only lengthen, and the random jitter no longer exceedsPROVIDER_RECOVERY_MAX_SECONDS. Temporary overloads reported as HTTP 529 (Anthropic “overloaded”), 520–524 (Cloudflare) or 425 now put the job in “waiting” with automatic resume; they used to fail it and require a manual restart. Waiting times also stop escalating after unrelated hiccups: the outage counter is reset by every successful provider call, not only at the end of a translated passage (#7). - Native Anthropic and OpenAI providers. The two provider types announced in 0.3.4 could not actually be created (the API and the settings screen rejected them) and, once wired, would have translated without your instructions: only the last system message reached Claude, so the translation prompt was replaced by the JSON-format reminder. They now go through the same path as every other provider, which brings what was missing: the full system prompt, token usage and costs in the statistics, the stored raw response, detection of refusals (
stop_reason: refusal) and truncated answers (max_tokens), the per-step temperature for OpenAI, and the configured output limit instead of a silent 8192 cap. For Claude, temperature and Top P are no longer sent because current Claude models reject them, and models are listed live from/v1/modelsin place of a hard-coded, outdated list. Both types accept a base URL with or without/v1, require an API key, and an emptychoicesanswer is handled as a provider error and no longer crashes the job (#8). - The “New provider” form no longer keeps the API key, model list and status message typed for the previously opened provider (#8).
- Error messages stay on screen. The red error banner used to vanish by itself — within five seconds in the library, within a second inside a book with a running job — because every automatic refresh cleared it. An export refusal or a save conflict could disappear before it was read. Automatic refreshes no longer erase or replace the error of an action; the banner stays until you dismiss it or start another action (#9).
- Sessions.
SESSION_DURATION_HOURSwas documented but ignored: sessions always lasted 12 hours. The setting is now applied to both the server-side session and the cookie (default 24 hours, as documented). When a session expires or is revoked, the interface returns to the login screen with an explicit message; it used to stay stuck on “Connexion nécessaire.” / “Reconnexion du suivi…”, and the Sign out button now works even if the session is already gone (#10). - Opening a book no longer replays its whole history. The live progress stream used to resend every past event of the book — thousands for a translated volume — making the workspace reload itself every two seconds for many minutes (up to about half an hour on the largest production book) and loading the server for nothing. A newly opened book now only receives what happens from that moment on; reconnections still resume exactly where they stopped (#12).
- “Configure selection” no longer rewrites settings you did not touch. Applying a common provider (or any other batch setting) to several books silently reset their target language to French and their quality to “High quality”. Both fields now default to “keep each book’s own value”, like the provider, memory source and instructions fields (#13).
- Database growth. Every model request was stored with its prompt three times over: once as the prompt, once more inside the saved request parameters, and again in the context trace — which also kept the full text of every context item that was not used. On a busy instance the
llm_requeststable grew by about 1.5 GB per day and filled the production disk, interrupting translations. New requests now store the prompt once; the trace still explains which context items were kept or dropped and why (source, relevance, size, reason, short excerpt for dropped items). The cache, the statistics and the request inspector are unaffected. Existing rows can be compacted withdocker compose exec api python -m app.maintenance.compact_request_logs(add--dry-runto measure first; see the module help for reclaiming disk space withVACUUM FULL) (#11). - Resuming on another provider. When a job runs on a recovery provider different from the book’s own, prompts are now sized for that provider’s context window. They used to be sized for the book’s configured provider — overflowing a smaller fallback model — and a job that brought its own provider to a book without one could never start (#14).
- Clearer API errors. Deleting a provider that is still in use answered “Cette entrée existe déjà, ou une référence est invalide”; it now says exactly what still refers to it (how many books, jobs or history requests) and what to do. When an external service — OpenViking memory, the Codex connector, SearXNG or a provider being tested — is down or unreachable, the affected action answers “service externe injoignable” (HTTP 502) where it used to show a generic server error with a diagnostic reference (#16).
- CI. The PostgreSQL migration job left a ~200 MB orphan database volume on the runner after every pipeline, which eventually filled the shared runner’s disk and made every job of every pipeline fail. The throwaway database now lives in memory and its container is removed together with its volumes (#20).
- Archived books stay at rest. Resuming or retrying an old job of an archived book, or choosing its first provider, could restart paid model calls on a book that was supposed to be shelved. Both are now refused or skipped until the book is restored. A failed job can no longer be “paused” — which used to turn it back into a blocking job and prevent any new work on the book — and starting a second job on a busy book answers with a proper conflict (HTTP 409) and no longer looks like invalid input (#15).
- EPUB import robustness. A failed import (validator failure, database error, or a project archive that does not match its EPUB) left the uploaded file behind on disk, one more copy per attempt; it is now removed with the rolled-back import. Books whose title, author or language metadata exceed the database limits are imported with truncated metadata and no longer end in a server error, and an EPUBCheck run that does not finish within 90 seconds no longer refuses the book: the informative source report is simply marked unavailable (at export time the same timeout now gives an explicit message) (#17).
- Interface details. The Strategy tab shows the book’s word count again (French showed none, English replaced the word “words” with the number); “Récupérer 1 passage(s)” is now properly singular or plural; raw keys such as
status.none,critique_acceptanceoralready_analyzedno longer appear in status lines; the book deletion prompt and button follow the interface language and the refresh time follows its locale; the same EPUB, project archive or glossary file can be selected again after a failed import without reloading the page; and the stage indicator returns to the live pipeline stage a few seconds after an export, where it used to stay stuck on “Export” (#18). - Resolving the last quality alert of a passage now clears its “À vérifier” flag, as accepting or rejecting a suggestion already did; the book no longer keeps reporting a passage to review when nothing is left to handle (#19).
Changed
- Unexpected API errors (HTTP 500) and failed jobs now log where they happened (
trace=with file, line and function) next to the diagnostic reference shown in the interface. Exception messages are deliberately left out so that service logs still never contain book text (#6).
[0.3.5] - 2026-09-17
Fixed
- The retry delay configured in Settings → Automatic recovery is now the one actually used when a provider is unavailable; the first retry waits exactly that delay and the exponential backoff starts from the second retry.
[0.3.4] - 2026-09-17
Added
- Native Anthropic Claude API provider (
kind: anthropic) supporting Claude 3.5 Sonnet, Haiku, Opus and earlier models with direct Messages API integration. - Native OpenAI ChatGPT API provider (
kind: openai_direct) supporting GPT-4 and GPT-3.5 models with direct Chat Completions API integration.
Changed
- Exponential backoff with jitter for provider retries (60s → 120s → 240s → 480s → 960s → 1920s → 3600s cap) to prevent rapid retry loops on persistent failures.
- Provider
Retry-Afterheaders now take priority over configured delays. - Added configurable retry parameters:
PROVIDER_RECOVERY_BASE_SECONDS(default 60),PROVIDER_RECOVERY_MAX_SECONDS(default 3600). - Added
SESSION_DURATION_HOURSconfiguration (default 24). - Structured error codes with retry eligibility and severity metadata.
Fixed
- Persistent timeout loops when Luna provider returns HTTP 504 at 600s limit (observed 254 retry attempts on Vol. 18).
[0.3.3] - 2026-09-16
Fixed
- Normalize hybrid EPUB 2 exports containing EPUB 3/HTML5 markup, invalid or duplicate XML IDs, and EPUB 3-only spine attributes so EPUBCheck accepts translated books while internal links remain valid.
[0.3.2] - 2026-09-16
Fixed
- Kept the current application available while the pre-deployment PostgreSQL dump runs, reducing the service interruption to the migration and image switch.
- Made deployment retries for an already healthy commit idempotent and hardened migration cancellation and rollback checks.
- Removed the blocking glossary-addition prompt from manual validation. A dedicated glossary panel now adds and locks terms explicitly.
[0.3.1] - 2026-09-14
Changed
- Aligned both interface themes with the Libris logo using midnight indigo, violet, pale lavender and coral tokens while preserving accessible contrast.
- Added a beginner-oriented Docker deployment path and a Docker operations guide.
- Made GitLab the canonical release pipeline: tested commit-addressed images are promoted to the GitLab Container Registry.
- Added a serialized, forced-command deployment from protected GitLab tags to the existing CT116 production stack, including a pre-deployment PostgreSQL backup and health verification.
- Allowed the destructive worker-recovery smoke test to target an explicitly named disposable Compose project.
[0.3.0] - 2026-09-14
Added
- Galley Proof visual system with tokenized typography, spacing, color, light/dark themes and reduced-motion behavior.
- Responsive global navigation and native workspace/chapter selectors for phone layouts.
- Keyboard focus trapping, Escape dismissal and trigger-focus restoration for inspectors and previews.
- Skip navigation, current-page semantics and 44 px phone touch targets.
Changed
- Consolidated the interface into one canonical stylesheet and replaced mobile library tables with readable records.
- Refreshed public screenshots from an isolated API-backed installation containing only fictional EPUB fixtures.
- Browser integration tests require a loopback
LIBRIS_E2E_URL, explicit credentials andLIBRIS_E2E_CONFIRM_DISPOSABLE=1; workspace tests acceptLIBRIS_E2E_PROJECT_IDorLIBRIS_E2E_STATEfor their prepared fixture.
Fixed
- Removed horizontal document overflow at tablet widths and restored access to every workspace section on narrow screens.
- Made the multi-book browser test follow the required archive-before-delete workflow.
[0.2.6] - 2026-09-14
Added
- Safe marker restoration for targeted revisions when unchanged text boundaries provide an unambiguous alignment.
- Adaptive reasoning fallback to
nonewhen reasoning consumes the response without producing usable final content.
Fixed
- Marker and truncation failures now enter the bounded small-batch repair path without five identical retries.
- Early failures report the actual number of attempts instead of a misleading
2/5suffix. - Validation failures retain their actionable internal reason instead of a generic
ValueErrorlabel.
[0.2.5] - 2026-09-14
Fixed
- Targeted revisions that alter immutable EPUB markers now receive one explicit marker-repair retry instead of five identical retries.
- Persistent marker violations retain the current translation and report an actionable reason in the quality issue.
[0.2.4] - 2026-09-14
Added
- Bulk ZIP download of complete translated EPUBs selected in the library.
Fixed
- Changing a project provider while work is paused now updates both the suspended job and any explicit recovery-provider override.
[0.2.3] - 2026-09-14
Added
- First-class per-provider reasoning control: unsupported, model default, disabled, minimal, low, medium, high or extra high.
- Specific diagnostics when a provider returns reasoning but no final content.
Fixed
- Reasoning settings now respect the declared capability consistently across Chat Completions, Responses and Codex transports.
[0.2.2] - 2026-09-14
Added
- Signed-in users can change their username while preserving active sessions.
Fixed
- Username changes reject conflicts with an existing account.
- The login username starts empty and browser autofill is disabled on the login form.
[0.2.1] - 2026-09-14
Security
- Patch four bundled EPUBCheck dependency JARs with SHA-256-pinned Jackson 2.18.8 and HttpCore 5.4.3 artifacts. Upstream EPUBCheck 5.3.0 remains the latest release; launcher filenames are preserved, updated versions are recorded in JAR metadata.
- Remove pip and its vendored build tooling from the runtime image after installation.
- These changes address seven fixable HIGH findings discovered by the full container scan after the application dependency checks.
[0.2.0] - 2026-09-14
Added
- Account page with password changes and individually revocable sessions.
- Administrator controls for roles, account deactivation/reactivation and password resets.
- Configurable provider recovery delay (5–3600 seconds, default 60), preserving checkpoints and provider capacity limits.
- Automatic analysis-to-translation pipeline, one targeted recovery pass and full AI review.
- Library sorting by status/model, visible status sort control and batch memory-source selection.
- Desktop/mobile account and recovery browser tests, and the full test suite on PostgreSQL in CI.
Fixed
- Chained jobs display their real translation/review stage instead of their initial analysis operation.
- Temporary provider failures preserve accepted corrections for retry.
- Obsolete correction-failure issues are closed when a successful correction clears the remaining critiques.
- Duplicate EPUB imports are detected per owner, including archived projects.
- Logout ends only the current session; password resets and account permission changes revoke all affected sessions.
Security and upgrade
- Unknown-account password checks use a dummy hash; password-change attempts are rate limited.
- Content Security Policy and browser permissions restrictions complement existing origin checks.
- Migration
e92fa613bc10addsusers.active, defaulting existing accounts to active. Run migrations before starting the new API/worker. - Provider retries now use the configured fixed delay rather than exponential backoff. Provider
Retry-Afterand concurrency limits still take precedence. Authentication errors, manual pauses and refusals are not automatically retried. - See audit and operational notes at the 0.2.0 tag for coverage and remaining limitations (repository access required).
[0.1.0] - 2026-09-13
Added
- Resumable EPUB analysis, translation, review and recovery jobs.
- Human review workspace, version history, glossary and character memory.
- Series-aware terminology and decisions, reversible project archives and batch controls.
- French and English interface catalogs.
- OpenAI-compatible, Responses API and optional Codex transports.
- Internal memory with optional OpenViking integration.
- EPUB reconstruction and EPUBCheck validation.
Security
- Non-root application containers, restricted capabilities and private database/bridge ports.
- Encrypted provider credentials, origin checks and hardened session cookies.
- Bounded EPUB archive parsing and synthetic-only public screenshots.