API d’automatisation
Cette page s’adresse aux développeurs qui veulent qu’un script ou un autre serveur envoie des livres à Libris et récupère la traduction, sans passer par l’interface web. Vous envoyez un EPUB, des chapitres TXT ou DOCX ou un document JSON ; Libris exécute seul tout le pipeline ; vous interrogez l’état (ou recevez un webhook) et téléchargez le résultat avec son rapport de fin.
L’API d’automatisation se trouve sous /api/v1 et est distincte de l’API qu’utilise l’interface web :
- elle n’accepte que des jetons d’API (
Authorization: Bearer …). Le cookie de session de l’interface n’ouvre pas/api/v1, et un jeton n’ouvre pas les routes de l’interface ; - le travail est asynchrone. Une demande est enregistrée en base avant la réponse
202 Accepted, le pipeline s’exécute dans le worker, et aucune connexion HTTP ne reste ouverte pendant une traduction ; - une demande aboutit toujours :
completed,completed_with_residuals,failed(avec la raison) oucancelled. Elle ne reste jamais indéfinimentrunning.
Notes culturelles pour le lecteur dans les livres exportés
Les notes relues appartiennent aux termes acceptés du glossaire d’un volume. Les résultats EPUB utilisent des notes
de bas de page EPUB 3 avec liens de retour ; les résultats texte utilisent des notes de fin de chapitre numérotées.
Les passages conservés dans la langue source ne sont jamais annotés.
Dans un export Word partiel, first désigne la première occurrence dans l’extrait sélectionné, sans charger les
chapitres situés hors de cette plage. Les extraits texte et EPUB de lecture conservent l’occurrence à l’échelle du volume.
L’EPUB bilingue n’annote que son côté traduit. Le texte des chapitres en JSON et les fichiers de chapitre du ZIP
contiennent des numéros de note et des explications lisibles, jamais les marqueurs d’annotation temporaires utilisés par l’exporteur.
Un EPUB 2 d’origine n’est converti en EPUB 3 que lorsqu’une note est insérée, en préservant les ressources de l’archive et
en passant le contrôle EPUBCheck habituel. Les exports existants restent inchangés quand aucune note n’est publiée.
Ces notes éditoriales sont gérées par des éditeurs authentifiés dans Glossaire › Notes du traducteur, non par
un export non authentifié ni par une réponse de modèle. L’API de session expose GET /api/projects/{pid}/reader-notes
et PUT /api/projects/{pid}/glossary/{gid}/reader-note ; cette dernière exige reader_note, previous_note
et note_placement (first, each, never), et accepte previous_placement et proposal_job pour
la détection des conflits. Une modification ou une proposition périmée renvoie 409. POST /api/projects/{pid}/jobs accepte
l’opération translator_notes, payante et asynchrone ; les propositions restent dans son résultat jusqu’à ce qu’elles soient explicitement relues.
Ce sont des routes de session : les jetons d’API ne les ouvrent pas.
En bref
| Méthode et chemin | Portée | Rôle |
|---|---|---|
POST /api/v1/translation-requests | content:write (+ pipeline:start pour traduire) | Envoyer un EPUB, des chapitres TXT ou DOCX ou un document JSON |
GET /api/v1/translation-requests/{id} | jobs:read | État, progression et rapport (?wait= pour l’attente longue) |
POST /api/v1/translation-requests/{id}/pause | jobs:control | Mettre en pause le travail de la demande |
POST /api/v1/translation-requests/{id}/resume | jobs:control | Le reprendre |
POST /api/v1/translation-requests/{id}/cancel | jobs:control | L’annuler |
GET /api/v1/translation-requests/{id}/result | results:read | Télécharger le résultat : ?format= (EPUB, EPUB bilingue avec ?layout=, JSON, TXT ou ZIP), ?scope= (les chapitres de la demande, seulement les nouveaux ou tout le volume), ?partial= et ?wait= (voir Récupérer le résultat) |
narrative:read | Lire le contexte narratif (texte et mémoire des livres) | GET /api/v1/books, GET /api/v1/books/{id}/narrative-context, GET /api/v1/books/{id}/narrative-context/revision, GET /api/v1/series/{id}/narrative-context |
GET /api/v1/providers | content:write | Lister les providers qu’une demande peut utiliser |
GET /api/v1/series | series:read | Lister vos séries |
GET /api/v1/series/{id} | series:read | Une série et ses volumes |
POST /api/v1/series/{id}/jobs/{action} | jobs:control | Suspendre, reprendre ou annuler une série, avec un rapport par volume ; voir Suspension persistante d’une série |
GET /api/v1/glossaries | series:read | Lister vos glossaires partagés |
POST /api/v1/glossaries | content:write | Créer un glossaire partagé |
GET /api/v1/glossaries/{id} | series:read | Un glossaire partagé et ses termes |
GET /api/v1/glossaries/{id}/export/{format} | series:read | Le télécharger en JSON, CSV ou TBX |
POST /api/v1/glossaries/{id}/import | content:write | Importer un fichier JSON, CSV ou TBX (?dry_run=true pour un aperçu) |
GET /api/v1/series/{id}/shared-glossary | series:read | Le glossaire partagé que suit une série |
PUT /api/v1/series/{id}/shared-glossary | content:write | Rattacher une série à un glossaire partagé, ou l’en détacher |
GET /api/v1/books | narrative:read | Lister vos livres |
GET /api/v1/books/{id}/narrative-context | narrative:read | Le Narrative Context Bundle d’un livre : son texte et sa mémoire (voir Contexte narratif) |
GET /api/v1/books/{id}/narrative-context/revision | narrative:read | Les empreintes du bundle seules |
GET /api/v1/series/{id}/narrative-context | narrative:read | La Series Bible, les identités de la série et ses volumes dans l’ordre de lecture |
Contexte narratif
Une autre application (Libris Draw, un script à vous) lit ce que Libris a compris d’un livre dans un seul
document, le Narrative Context Bundle, au lieu de lire la base de données. Il lui faut un jeton portant
la permission narrative:read, et rien d’autre ; la lecture ne lance aucun travail et ne compte aucun mot.
curl -H "Authorization: Bearer $LIBRIS_TOKEN" "$LIBRIS_URL/api/v1/books/$BOOK_ID/narrative-context"
Le bundle contient le livre et sa série, content.chapters (chaque chapitre avec ses passages : source, la
translation en vigueur, le summary de l’analyse et les character_ids qu’elle nomme) et narrative : la
Book Bible, les fiches des personnages (alias, description, mentions, et observations avec les mots du
texte sur lesquels elles reposent), leurs relations, les lieux, organisations et objets que la bible cite
(world), le glossaire et les événements de chaque passage. Les identifiants sont ceux de Libris et ne
changent jamais. Chaque élément porte une provenance : origin vaut analysis quand le modèle l’a écrit et
human quand une personne l’a validé, pour distinguer un fait du livre d’une supposition.
?chapter_id= (répétable) limite le texte à certains chapitres, ?text=none omet le texte ; la mémoire
narrative est toujours entière.
Un bundle est un instantané. Il est versionné par schema_version (la forme, 1), generated_at, checksum
(SHA-256 de son corps) et trois empreintes : revision pour la mémoire narrative, content_revision pour le
texte, et une revision par chapitre. Ce sont des empreintes, pas des compteurs : égales, rien n’a changé.
Pour savoir si un instantané pris plus tôt est encore à jour, comparez-les à
GET …/narrative-context/revision, qui ne répond que les empreintes. Libris ne pousse jamais un changement
vers un client : c’est le client qui décide de prendre un nouvel instantané.
GET /api/v1/series/{id}/narrative-context répond ce qui vaut pour tous les volumes : la Series Bible, les
identités canoniques (le series_entity_id d’un personnage dans le bundle d’un livre désigne l’une d’elles),
les relations et le glossaire de la série, et les volumes dans l’ordre de lecture.
Piloter Libris depuis un agent de code (MCP)
En plus de cette API, Libris répond au Model Context Protocol sur POST /mcp, avec les mêmes jetons
et les mêmes portées qu’ici. Un client MCP — opencode, Claude Code, Codex — peut alors consulter la bibliothèque,
préparer un livre, tenir le glossaire et diriger le travail avec ses propres mots. 28 outils, les
permissions du jeton décidant lesquels répondent. Voir mcp.fr.md.
claude mcp add --transport http libris "$LIBRIS_URL/mcp" --header "Authorization: Bearer $LIBRIS_TOKEN"
Description OpenAPI et client d’exemple
-
docs/openapi/libris-v1.jsondécrit/api/v1en OpenAPI 3.1 : chaque opération avec sa portée, ses paramètres, ses corps (document JSON, envoi multipart, EPUB brut), ses réponses, ses codes d’erreur et ses exemples, ainsi que le webhook. Chargez-le dans n’importe quel outil OpenAPI ou générateur de client. Il est généré à partir du code et vérifié par la suite de tests : il correspond donc toujours à la version de Libris avec laquelle il est livré. Le site web le présente sous forme de référence lisible. -
examples/libris_client.pyest un petit client Python qui n’utilise que la bibliothèque standard (Python 3.10 ou plus récent, rien à installer). Il envoie un EPUB, des chapitres TXT ou DOCX ou un document JSON avec n’importe quelle option d’envoi, attend par attente longue, télécharge le résultat dans n’importe quel format et périmètre, patiente lors des réponses429 rate_limited(il signale429 queue_fullau lieu de réessayer), et reçoit les webhooks après avoir vérifié leur signature :export LIBRIS_URL=https://libris.example.org LIBRIS_TOKEN=lbr_xxxxxxxx_... python3 examples/libris_client.py epub book.epub --target-language fr --provider-id "$PROVIDER_ID" --out book.fr.epub python3 examples/libris_client.py chapters "Chapter 1.txt" "Chapter 2.txt" --series "Web Saga" --volume 1 \ --source-language en --target-language fr --format txt-zip --out volume-1.zip python3 examples/libris_client.py chapters "Chapter 41.txt" --series "Web Saga" --volume latest \ --source-language en --target-language fr --format epub-bilingual --layout side-by-side --scope new LIBRIS_WEBHOOK_SECRET=... python3 examples/libris_client.py webhooks --port 8080Sa classe
LibrisClientpeut aussi être importée dans votre propre code ; lancez-le avec--helppour voir toutes les options. Le client n’envoie le jeton qu’à l’origine deLIBRIS_URL: il ne suit une redirection que si le schéma, l’hôte et le port restent identiques (cinq au plus d’affilée) et refuse toute autre — autre serveur, autre port, ou passage dehttpsàhttp— avec le code d’erreurredirect_refused: ni le jeton ni le livre ne partent ailleurs.
Tous les exemples de cette page utilisent ces variables shell :
export LIBRIS_URL=https://libris.example.org
export LIBRIS_TOKEN=lbr_xxxxxxxx_... # affiché une seule fois, à la création du jeton
export PROVIDER_ID=... # un id de provider, voir « Choisir un provider » plus bas
Créer un jeton
- Dans l’interface, ouvrez Mon compte › Jetons d’API (les administrateurs le trouvent aussi sous Paramètres › API d’automatisation).
- Sous Créer un jeton, donnez-lui un nom, cochez les permissions dont il a besoin et choisissez une expiration (30, 90 ou 365 jours, ou jamais).
- Si vous le souhaitez, cochez Signer les webhooks avec un secret propre à ce jeton (voir Webhooks), réglez ses limites de File d’attente (voir Priorité et quotas de la file d’attente) et donnez-lui un Budget du jeton (voir Budget du jeton).
- Copiez le secret tout de suite : il est affiché une seule fois, jamais plus.
Un jeton agit au nom de son propriétaire : il ne voit que les séries et les demandes du propriétaire, et il cesse
de fonctionner quand le compte du propriétaire est désactivé. La liste affiche pour chaque jeton son préfixe, ses permissions,
ses dates de création et d’expiration, sa dernière utilisation (mise à jour au plus une fois par minute) et son état (actif, expiré,
révoqué). Révoquer est immédiat et définitif ; les clients qui utilisent le jeton reçoivent alors 401. La création et
la révocation d’un jeton sont inscrites au journal d’audit, sans le secret. Un compte peut détenir au plus 50
jetons non révoqués.
| Portée | Libellé dans l’interface | Autorise |
|---|---|---|
series:read | Lire les séries | GET /api/v1/series, GET /api/v1/series/{id}, GET /api/v1/glossaries, GET /api/v1/glossaries/{id}, GET /api/v1/glossaries/{id}/export/{format}, GET /api/v1/series/{id}/shared-glossary |
content:write | Envoyer du contenu | POST /api/v1/translation-requests, GET /api/v1/providers, POST /api/v1/glossaries, POST /api/v1/glossaries/{id}/import, PUT /api/v1/series/{id}/shared-glossary |
pipeline:start | Lancer le pipeline | Avec content:write : les demandes qui lancent la traduction (le comportement par défaut). Sans elle, seul start=false (import seul) est accepté. |
jobs:read | Suivre les travaux | GET /api/v1/translation-requests/{id} |
jobs:control | Piloter les travaux (pause, reprise, annulation) | POST …/pause, …/resume, …/cancel |
results:read | Lire les résultats | GET /api/v1/translation-requests/{id}/result |
Un jeton a la forme lbr_ + 8 caractères d’identification + _ + un secret aléatoire (256 bits). Libris
n’en stocke que le SHA-256 et le compare en temps constant.
Gérer les jetons depuis un script
L’interface gère les jetons au moyen de ces routes, qui utilisent le cookie de session, et non un jeton :
| Route | Corps et réponse |
|---|---|
GET /api/tokens | Les jetons de l’appelant : id, name, prefix, scopes, created_at, expires_at, revoked_at, last_used_at, state, webhook_secret (un booléen : le jeton a-t-il son propre secret de signature), max_priority, max_running, max_queued (voir Priorité et quotas de la file d’attente) et budget (null sans plafond, sinon {amount, period, spent, resets_at}, voir Budget du jeton). |
POST /api/tokens | Corps {"name": "…", "scopes": ["…"], "expires_in_days": 90, "webhook_secret": false, "budget_amount": null, "budget_period": "month"}, avec éventuellement max_priority (low, normal (par défaut) ou high), max_running (1–1000) et max_queued (1–100000). name de 1 à 100 caractères, au moins une portée, expires_in_days de 1 à 3650 ou null pour aucune expiration, budget_amount un plafond positif ou null, budget_period month ou total. Répond 201 avec la vue du jeton, plus token (le secret) et, si demandé, webhook_secret (le secret de signature). C’est la seule réponse qui les contienne jamais. 409 dès que le compte détient 50 jetons non révoqués ; 403 priority_not_allowed quand max_priority dépasse le plafond propre au compte. |
PUT /api/tokens/{id}/queue | Corps {"max_priority": "normal", "max_running": null, "max_queued": null} : modifie les limites de file d’attente du jeton sans changer son secret ; renvoie sa vue. 403 priority_not_allowed quand max_priority dépasse le plafond propre au compte. |
PUT /api/tokens/{id}/budget | Corps {"amount": 50, "period": "month"} (amount: null supprime le plafond). Renvoie la vue du jeton. Inscrit au journal d’audit. |
DELETE /api/tokens/{id} | Révoque le jeton et renvoie sa vue. |
Priorité et quotas de la file d’attente
Libris partage ses providers entre les comptes grâce à une file d’attente équitable : les travaux en attente démarrent par priorité, puis à partir
du compte (et du jeton) qui a le moins de travaux en cours, à tour de rôle entre les comptes ; les volumes d’une
série passent un par un, dans l’ordre de lecture (voir architecture (en anglais)). Une demande peut réclamer une priorité, pipeline.priority dans un document
JSON ou l’option priority d’un envoi de fichier : low, normal (par défaut) ou high.
- La priorité ne peut dépasser ni le
max_prioritydu jeton (normalsauf réglage contraire) ni le plafond du compte (highpour les administrateurs et pour les comptes qu’un administrateur a autorisés dans Paramètres › File d’attente,normalsinon ; un administrateur peut aussi y abaisser un compte àlow). Au-delà, la demande est refusée avec403 priority_not_allowedetmax_prioritydans l’erreur. Sans priorité, une demande s’exécute ennormal, ou au plafond inférieur d’un jeton ou d’un compte limité àlow. max_runninglimite les travaux du jeton exécutés simultanément : les suivants attendent (queue.reasonvauttoken_limit). La limite propre au compte (QUEUE_MAX_RUNNING_PER_ACCOUNTou sa ligne dans Paramètres › File d’attente) s’applique aussi (account_limit).max_queuedlimite les demandes et travaux du jeton qui attendent de démarrer. Une nouvelle demande qui le dépasse, ou qui dépasse le quota d’attente du compte, est refusée avec429 queue_fulletscope(tokenouaccount) etlimitdans l’erreur ; rien n’est enregistré. Réessayez une fois que l’un d’eux a démarré, pas immédiatement : contrairement àrate_limited, cette réponse n’a pas deRetry-After. Le rejeu d’une demande acceptée (mêmeIdempotency-Keyou mêmeexternal_id) reçoit toujours une réponse. Reprendre une demande en pause compte comme une nouvelle entrée dans la file, donc…/resumepeut lui aussi répondre429 queue_full.
La priorité ne change pas le contenu de la demande : renvoyer la même demande avec une autre priorité est un rejeu de la première.
Budget du jeton
Un jeton peut avoir un plafond de dépenses, dans la devise dans laquelle les prix des providers sont saisis (celle de
usage.cost dans les rapports). Il compte les appels au modèle des demandes faites avec le jeton, par
mois civil (UTC, period: "month") ou sur toute la durée de vie du jeton (period: "total"). Une
demande terminée compte pour le coût conservé sur elle, dans le mois où elle s’est terminée ; une demande en cours compte
ce que ses appels ont coûté jusqu’ici.
- Une fois le plafond atteint, une nouvelle demande qui lancerait du travail (
startvrai, par défaut) est refusée avec402 budget_exceeded; l’erreur portebudget: {amount, spent, period, resets_at}(resets_at: début du mois suivant,nullpour un plafond total). Un import seul (start: false) ne coûte rien et reste accepté. - Une demande en cours dont le jeton approche de son plafond est traitée comme un livre proche de son budget (voir
budgets de coût) : son travail passe à un provider de repli moins cher,
ou se met en pause avec la raison d’arrêt
budget_exceeded.…/resumerépond409 budget_exceededtant que le plafond n’a pas été relevé (PUT /api/tokens/{id}/budget). Une demande laissée en pause plus longtemps queAPI_REQUEST_STALL_MINUTESéchoue avec cette raison. - Les plafonds sont vérifiés de nouveau quand le travail de la demande démarre (tout de suite, ou dès que le volume est libre) :
le budget du livre et celui du jeton, un plafond dépassant le seuil de bascule sans provider moins cher, et,
quand l’installation refuse les lancements dont l’estimation dépasse ce qui reste (
BUDGET_ON_ESTIMATE=refuse), l’estimation. Une demande acceptée avec202puis refusée à ce stade se termine enfailed, avec la raison danserror. - Les appels synchrones en cours réservent une marge de budget sur l’ensemble des travaux du même jeton, y compris ceux qui utilisent des providers différents. Près du seuil de bascule, l’appel payant suivant attend que ces appels se règlent ; la décision ordinaire (provider moins cher ou pause) est vérifiée de nouveau juste avant l’admission. Cela préserve la règle existante du dernier appel indivisible, et non une garantie de porte-monnaie prépayé : un seul appel peut franchir le plafond. Une annulation ou un délai dépassé ne peut pas non plus rembourser un travail déjà envoyé au provider. Des jetons différents gardent des plafonds monétaires distincts ; les allocations de mots du compte et les quotas de file d’attente sont distincts.
Quota de mots de la licence
Le quota de mots de la licence est celui de l’installation, pas du jeton : il compte les mots source, une sur le cycle de quota de la licence (un mois ancré sur la date d’abonnement ; le mois civil avec un serveur de licences plus ancien). Ce qu’un cycle a dépassé est déduit du suivant.
- Un livre est compté dès son ajout (0.17.0) : tous ses mots, à l’import, même s’il est supprimé ensuite sans
avoir été traduit. Le traduire, le relancer ou refaire un passage ne coûte ensuite plus rien. Une demande dont
les chapitres ne tiennent pas dans ce qui reste du cycle plus la marge
LICENCE_QUOTA_OVERRUN_WORDS(20 000 mots par défaut) est refusée en entier avec402 licence_quota_insufficient— ou402 allowance_insufficientpour le quota mensuel propre au compte — avecwordsetleft(marge comprise) ; rien n’est importé. Renvoyez-la quand le quota le permet. - Un livre ajouté avant la 0.17.0 reste compté passage par passage au fil de sa traduction ; s’il ne tient pas
dans ce qui reste du cycle, il n’entre pas dans la file, et une demande qui le lancerait se termine en
failedavecstop_reason: "licence_quota_insufficient". - Le démarrage d’une série entière (
…/series/{id}/jobs/start) répond, pour chaque volume refusé,code: "licence_quota_insufficient": chaque volume doit tenir dans ce que les volumes lancés avant lui dans le même appel ont laissé. - L’outil MCP
start_job, commePOST /api/projects/{id}/jobsdans le navigateur, répond402 licence_quota_insufficientpour la traduction d’un livre entier, avecwords(ce que le livre coûterait) etleft. - Une licence sans limite de mots ne refuse jamais. Un livre déjà compté se traduit même une fois le quota atteint ; seule la comparaison de providers, qui rappelle les modèles, est comptée et arrêtée.
Envoyer une demande de traduction
POST /api/v1/translation-requests accepte trois types d’entrée :
| Entrée | Comment l’envoyer | Résultat par défaut |
|---|---|---|
| Un EPUB | multipart/form-data avec un fichier .epub dans le champ file, les options en champs de formulaire ; ou le fichier brut en Content-Type: application/epub+zip, les options dans la chaîne de requête | L’EPUB traduit |
| Des chapitres TXT ou DOCX | multipart/form-data avec un ou plusieurs fichiers .txt ou .docx dans file ou files, les options en champs de formulaire ; un fichier peut être découpé à ses titres de chapitre | JSON |
| Un document JSON | Content-Type: application/json ; ou un fichier .json dans le champ multipart file (sans aucun autre champ de formulaire) | JSON (ou output.format) |
Une demande transporte un seul type de fichier : mélanger des fichiers .epub, .txt, .docx et .json, ou envoyer
plusieurs fichiers EPUB ou JSON, est refusé avec 422. Les fichiers Markdown et HTML ne sont pas acceptés ici : importez-les
par l’interface.
Toute demande acceptée répond 202 Accepted, avec un en-tête Location qui pointe vers son état :
{
"request_id": "5b1c…",
"external_id": "tbate-volume-12",
"series_id": "…",
"project_id": "…",
"job_id": "…",
"input": "json",
"status": "pending",
"status_url": "/api/v1/translation-requests/5b1c…",
"result_url": "/api/v1/translation-requests/5b1c…/result"
}
input vaut epub, txt, docx ou json. job_id vaut null tant que la demande attend son volume
(status: "queued").
Choisir un provider
Une traduction a besoin d’un provider de modèle. Libris utilise, dans l’ordre, le provider_id de la demande, le
provider du volume, puis le provider par défaut de la série. Si aucun n’est défini, la demande est refusée avec
422 provider_required ; un id inconnu répond 422 unknown_provider.
Le plus simple est de choisir une fois pour toutes un provider par défaut pour la série, dans l’interface (onglet
Paramètres par défaut de la série), et d’omettre provider_id. Pour connaître les id des providers, listez les providers avec un jeton
qui a la portée content:write :
curl -sS "$LIBRIS_URL/api/v1/providers" -H "Authorization: Bearer $LIBRIS_TOKEN"
[{"id": "…", "name": "Local", "kind": "openai", "model": "qwen3-32b", "created_at": 1789000000.0,
"default_for_series": ["…"]}]
La liste est triée par nom. kind est le type de connexion (openai, openai_direct, openai_responses,
anthropic ou codex_chatgpt), et default_for_series liste les id de vos séries qui utilisent ce
provider par défaut. L’adresse et la clé d’API du provider ne sont jamais incluses. Les providers sont gérés par les
administrateurs dans l’interface.
Envoyer un EPUB
# Multipart : le fichier et ses options en champs de formulaire
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests" \
-H "Authorization: Bearer $LIBRIS_TOKEN" \
-H "Idempotency-Key: silver-tower-fr-1" \
-F "file=@The Silver Tower.epub;type=application/epub+zip" \
-F series="Silver Saga" -F volume=1 \
-F source_language=en -F target_language=fr \
-F provider_id="$PROVIDER_ID" -F quality=high
# Corps brut : les options dans la chaîne de requête
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests?target_language=fr&provider_id=$PROVIDER_ID&filename=tower.epub" \
-H "Authorization: Bearer $LIBRIS_TOKEN" \
-H "Content-Type: application/epub+zip" \
--data-binary @tower.epub
Ce que Libris en fait :
- L’EPUB devient un volume : isolé, ou dans la série désignée par
series(créée si elle n’existe pas) ou parseries_id. Son pipeline démarre aussitôt. - Le même fichier envoyé de nouveau (mêmes octets) réutilise le volume déjà créé à partir de lui, et la demande
enregistre cette décision. Un volume archivé répond
409 volume_archived. - Dans une série, sans
volume, Libris prend le numéro de volume dans le nom du fichier quand ce numéro est libre, sinon le numéro qui suit le dernier volume. Un nom de fichier qui signale un volume spécial — un+ou une décimale après le numéro (LN 14+,Vol. 3.5) — ne reçoit aucun numéro : le volume est créé sans numéro, car deviner 14 entrerait en collision avec le vrai volume 14. Le choix et sa raison sont consignés dans le rapport (decisions.intake). Un numéro de volume déjà utilisé par un autre livre répond409 volume_conflict. - Les langues sont par défaut la langue déclarée dans l’EPUB (
ens’il n’y en a pas) et la langue cible de la série (frs’il n’y en a pas). - Chaque chapitre est nommé d’après son titre —
kind,number,part,label— exactement comme un chapitre envoyé en JSON (voir Chapitres irréguliers) ; l’ordre propre de l’EPUB est l’ordre de lecture et n’est jamais modifié, et sa table des matières et ses pages annexes restent en dehors de la numérotation. Ce qui a été détecté figure dans le document d’état (chapters[].mapping) et dans le rapport de fin (chapter_map). - Un fichier qui ne peut pas être lu comme un EPUB répond
422 invalid_epub; un EPUB à mise en page fixe (pre-paginated), que Libris ne peut pas traduire sans que le texte déborde de ses pages fixes, répond422 fixed_layout_epub. Quand seules certaines de ses pages de texte sont fixes, l’EPUB est accepté et la décision dit combien. - Si un travail est déjà en cours sur le volume, ses réglages ne sont pas touchés et la demande l’attend (la décision est consignée).
Envoyer des chapitres TXT ou DOCX
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests" \
-H "Authorization: Bearer $LIBRIS_TOKEN" \
-F "files=@Chapter 1.txt" -F "files=@Chapter 2.txt" -F "files=@Afterword.txt" \
-F series="Web Saga" -F volume=1 \
-F source_language=en -F target_language=fr \
-F provider_id="$PROVIDER_ID" -F output_format=txt-zip
Chaque fichier devient un chapitre, et la demande se comporte exactement comme le document JSON qu’elle représente
(même idempotence, même recherche du volume, mêmes conflits et mêmes résultats). series (ou series_id), volume,
source_language et target_language sont obligatoires.
- Encodage. Les fichiers sont décodés en UTF-8, en UTF-16 avec BOM ou, en dernier recours, en Windows-1252. Ce dernier cas est consigné dans le rapport.
- Les numéros de chapitre viennent des noms de fichier (
Chapter 12.txt,012 - Title.txt, ou la partie qui varie d’un fichier à l’autre du lot). Un fichier sans numéro, ou avec le même numéro qu’un autre fichier, reçoit le prochain numéro libre dans l’ordre d’envoi. Ces choix ne donnent jamais lieu à des questions : chacun est consigné avec sa raison dansreport.decisions.intake. - Carte des chapitres. Les noms donnent aussi les parties et les chapitres spéciaux, comme dans le
document JSON :
Chapter 12 - Part 2.txt,Ch12 (2-2).txt,12a.txt/12b.txt,12.1.txtavec12.2.txt(des parties quand les deux sont envoyés sans chapitre 12),第12章(下).txt;Prologue.txt,Interlude – Ayla.txt,Side Story 3.txt,Afterword.txt,Author's Note.txt… Un chapitre spécial n’a pas besoin de numéro : un prologue vient en premier, un épilogue ou une postface en dernier, un autre chapitre spécial après le chapitre qui le précède dans l’ordre des noms ; une place qui ne repose que sur l’ordre alphabétique est consignée dansreport.decisions.intakeavec une confiance faible. - Les titres sont tirés des noms de fichier.
- Les fichiers DOCX s’envoient de la même façon (
.docxau lieu de.txt, un seul type par demande) : chacun devient un chapitre texte composé de ses paragraphes, un par ligne ; la mise en forme n’est pas conservée.
Un fichier contenant plusieurs chapitres
Un webnovel arrive souvent sous la forme d’un seul gros fichier. Envoyez-le seul avec split=headings et Libris le découpe à ses
titres de chapitre, sans aperçu :
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests" \
-H "Authorization: Bearer $LIBRIS_TOKEN" \
-F "file=@The Glass Road.txt" -F split=headings \
-F series="Glass Road" -F volume=1 \
-F source_language=en -F target_language=fr
- Titres. D’abord les styles de titre DOCX (
Heading 1,Titre 1…) ; sinon des lignes commeChapter 12,Chapitre 12 : Title,CHAPTER XII,Chapter One,Chapitre Premier,第12章,Prologue,Epilogue,Interlude – Ayla,Side Story 2: Title,Author's Note,番外一; sinon des lignes numérotées (1. Title,2. Title…), seulement quand elles se suivent. Une phrase qui mentionne un chapitre ne provoque pas de découpe, et une table des matières (des titres sans texte entre eux) est ignorée. Les nombres cardinaux et ordinaux écrits en toutes lettres, en français et en anglais, sont reconnus jusqu’à 999, avec espaces ou traits d’union (Twenty-One,quatre-vingt-dix-neuf). Au-delà, un nombre en toutes lettres exige un numéro explicite ; unOne Thousandnon pris en charge n’est jamais lu silencieusement comme le chapitre 1. - Chapitres. Chaque titre ouvre un chapitre qui porte ce titre ; son texte suit le titre. Le texte qui précède
le premier titre devient un chapitre liminaire (
kindfront_matter), nommé dans la langue source du volume plutôt que dans la langue de l’interface, quand il contient des mots. Les numéros viennent des titres ;Chapter 12 (1/2)etChapter 12 (2/2)sont les deux parties du chapitre 12. Un prologue, un interlude, une histoire annexe ou un épilogue garde sonkindet sa place dans le fichier, sans numéro de chapitre. Un titre stylé qui ne nomme aucun chapitre est numéroté entre ses voisins. Un titre dont le numéro revient en arrière reste dans le chapitre précédent. - Rapport. La découpe est une décision de
report.decisions.intake(split: le nombre de chapitres, avec la raison) ; les numéros manquants et les titres ignorés y sont aussi listés. Un fichier qui compte moins de deux titres reste un seul chapitre, et c’est également consigné. - État.
chapters.itemsdu document d’état liste les chapitres créés, avec leur numéro et leur titre. Renvoyer le même fichier retrouve les mêmes chapitres (unchanged), comme pour tout chapitre. split=headingsprend exactement un fichier (422 invalid_payloadsinon) et est refusé pour un EPUB. Sans lui (split=none, par défaut), chaque fichier reste un seul chapitre.
Envoyer un document JSON
{
"external_id": "tbate-volume-12",
"series": {"id": null, "name": "The Synthetic Saga", "create_if_missing": true},
"volume": {"external_id": "volume-12", "number": 12, "title": "Volume 12"},
"author": "A. Author",
"source_language": "en",
"target_language": "fr",
"chapters": [
{"external_id": "chapter-001", "number": 1, "title": "Chapter 1",
"content": "First paragraph.\n\nSecond paragraph.\n"}
],
"replace_changed_chapters": false,
"discard_human": false,
"pipeline": {"start": true, "provider_id": null, "quality": "high",
"context_backend": "hybrid", "final_review": true, "priority": "normal"},
"output": {"format": "json"},
"callback_url": "https://hooks.example.org/libris"
}
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests" \
-H "Authorization: Bearer $LIBRIS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tbate-volume-12-run-1" \
--data @request.json
# Le même document, envoyé comme fichier
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests" \
-H "Authorization: Bearer $LIBRIS_TOKEN" \
-H "Idempotency-Key: tbate-volume-12-run-1" \
-F "file=@request.json;type=application/json"
| Champ | Obligatoire | Règles |
|---|---|---|
external_id | non | Votre identifiant pour la demande : commence par une lettre ou un chiffre, puis des lettres, des chiffres et ._:/-, jusqu’à 200 caractères. Unique par propriétaire (voir Envoyer deux fois). |
series | oui | id (une série qui vous appartient) ou name. Un nom inconnu est créé quand create_if_missing est vrai (par défaut), sinon 404 series_not_found. Une série archivée répond 409 series_archived. |
volume.number | oui, sauf avec volume.latest | 1–10000. |
volume.latest | non | false par défaut. true à la place d’un numéro : les chapitres vont au dernier volume de la série (le numéro le plus élevé ; le flux continu de chapitres de la série quand elle n’a aucun volume numéroté ; le volume 1, créé, quand elle n’a ni l’un ni l’autre). Voir Suivre une série dans la durée. |
volume.external_id, volume.title | non | Le volume est retrouvé par external_id, puis par numéro dans la série ; sinon il est créé (titre par défaut : « Série — numéro »). Un volume issu d’un EPUB répond 409 volume_conflict, de même qu’un volume portant ce numéro mais un autre external_id. Un volume archivé répond 409 volume_archived. |
author | non | Jusqu’à 500 caractères. |
source_language, target_language | oui | Balises BCP 47 comme en, fr-FR, zh-Hant, es-419. Appliquées au volume. |
chapters | oui | De 1 à API_MAX_CHAPTERS (2000) chapitres. number (0–100000, décimales comme 12.5 autorisées) est obligatoire pour un chapitre, sauf si son titre le donne ; un prologue, un interlude ou un autre chapitre spécial n’en a pas besoin (voir Chapitres irréguliers). Les numéros (avec leur part) et les external_id ne doivent pas se répéter. content ne doit pas être vide et contient au plus TEXT_CHAPTER_MAX_CHARS caractères. title est facultatif ; sans lui, rien n’est ajouté au texte et les exports nomment le chapitre par son libellé (Chapitre 12, Prologue). Les chapitres sont ordonnés par numéro et par partie, un chapitre spécial après le chapitre qu’il suit dans cette liste. |
replace_changed_chapters | non | false par défaut : un chapitre déjà présent dans le volume (même external_id ou même numéro) avec un texte différent répond 409 chapter_conflict, avec la liste dans conflicts. Avec true, il est remplacé ; les passages dont le texte n’a pas changé gardent leur traduction. |
discard_human | non | false par défaut : un remplacement qui ferait perdre des passages corrigés ou validés par une personne répond 409 conflict avec protected_segments. Avec true (et replace_changed_chapters), ces modifications sont abandonnées. |
pipeline.start | non | true par défaut : exécuter tout le pipeline (exige la portée pipeline:start). false se contente d’importer les chapitres. |
pipeline.provider_id | non | Voir Choisir un provider. |
pipeline.quality | non | fast, normal, high ou maximum (voir le guide du pilote automatique). |
pipeline.context_backend | non | internal, openviking ou hybrid (voir OpenViking). |
pipeline.final_review | non | true par défaut. false saute la relecture finale. Elle ne s’exécute jamais quand le serveur définit FINAL_REVIEW_ENABLED=false. |
pipeline.priority | non | low, normal (par défaut) ou high, dans la limite du plafond du jeton (voir Priorité et quotas de la file d’attente). |
pipeline.analysis_mode | non | parallel ou strict ; par défaut : le choix du volume, sinon ANALYSIS_MODE (parallel). Voir Modes d’analyse. |
pipeline.threads | non | 1–64 : nombre de passages de ce volume traités simultanément, pour l’analyse comme pour la traduction. Il ne peut que réduire la part de la capacité du provider attribuée au volume. Par défaut : le choix du volume, sinon cette part. |
pipeline.escalation_provider_id | non | Le modèle supérieur du volume, réservé à ses cas difficiles : un passage qui revient à l’arbitrage IA, ou qu’aucun échelon de la récupération n’a pu traduire. Aucune étape du livre ne bascule sur lui. C’est un réglage du volume : il vaut pour les demandes suivantes. Une chaîne vide l’efface ; par défaut : inchangé, puis le choix propre du serveur. |
output.format | non | Format par défaut du résultat : json, txt, txt-zip ou epub-bilingual. |
callback_url | non | Un webhook appelé quand la demande se termine (voir Webhooks). |
callback_events | non | Événements de webhook supplémentaires, en plus de l’événement final : ["chapters.translated"] envoie un lot chaque fois que des chapitres de la demande sont traduits (voir Lots de chapitres traduits). 5 éléments au plus. Utilisé seulement avec un callback_url : sans lui, les événements sont acceptés puis ignorés. |
Les champs inconnus sont refusés. Libris ne télécharge jamais rien depuis une URL trouvée dans le document : le texte est pris tel quel. Les chapitres passent par le même importeur de texte que les fichiers TXT (mêmes passages, même mise en page et mêmes sommes de contrôle), et le document normalisé est stocké comme fichier source du volume.
Un nouveau volume, et une série créée par la demande, prennent provider_id, quality et
context_backend dans pipeline quand ils y figurent, sinon dans les valeurs par défaut de la série.
Chapitres irréguliers : parties, prologues, interludes
Les webnovels publient des chapitres en plusieurs parties et ajoutent des prologues, des interludes, des histoires annexes, des chapitres
bonus, des postfaces et des notes de l’auteur entre les chapitres numérotés. Chaque chapitre du document
peut dire ce qu’il est ; tout ce qui est omis est déduit de son title :
"chapters": [
{"external_id": "p0", "kind": "prologue", "content": "…"},
{"external_id": "c12a", "number": 12, "part": 1, "part_count": 2, "content": "…"},
{"external_id": "c12b", "number": 12, "title": "Chapter 12 (Part 2)", "content": "…"},
{"external_id": "i1", "title": "Interlude – Ayla", "content": "…"},
{"external_id": "c13", "number": 13, "content": "…"},
{"external_id": "ss3", "kind": "side_story", "number": 3, "label": "The Past", "after": "c12b", "content": "…"}
]
| Champ | Signification |
|---|---|
kind | chapter (par défaut), prologue, interlude, side_story, extra, epilogue, afterword, author_note ou front_matter. Absent : déduit du titre (Prologue, Épilogue, Interlude – Ayla, Side Story 3, 番外, Author's Note…) ; un titre numéroté comme Chapter 12: Prologue to War reste un chapitre. |
number | Pour un chapitre, son numéro (12.5 est un vrai demi-chapitre). Pour un chapitre spécial, son propre numéro (Interlude 2 est le deuxième interlude, pas le chapitre 2) ; un chapitre spécial ne participe pas à la numérotation des chapitres. |
part, part_count | Partie d’un chapitre publié en plusieurs (1–999) et, quand on le sait, son nombre de parties (part ne peut pas dépasser part_count). Absent : déduit du titre (Chapter 12 (Part 2), Ch12 (2/2), Chapitre 12 partie 2, 第12章(下)) quand il nomme le même chapitre. |
label | Le nom affiché après le type (Ayla dans Interlude – Ayla), 200 caractères au plus. |
after | Insérer après ce chapitre : son chapter_id Libris, son external_id (dans le volume ou dans cette demande), un numéro de chapitre (après ce chapitre, ses parties et les chapitres spéciaux déjà placés après lui) ou start. Une valeur inconnue répond 422 invalid_placement. Envoyé avec un chapitre déjà présent dans le volume, il le déplace. |
position | Insérer à cette position de lecture (0–100000 ; 0 : en premier). Pas avec after. |
Ordre de lecture. Les chapitres d’un volume sont ordonnés selon leur position de lecture, non selon leurs numéros :
l’analyse, le contexte et la mémoire (un passage ne voit que ce qui le précède), l’analyse parallèle, les règles
anti-divulgâchage, les scores de qualité, les suites et les exports la suivent tous. Sans after ni position, un
chapitre numéroté se place après les chapitres de numéro ou de partie inférieurs : une partie 2 envoyée après la traduction
de sa partie 1 se place juste après celle-ci (et la voit comme son contexte précédent). Un chapitre spécial se place après le
chapitre qu’il suit dans la demande ; un prologue ou un chapitre liminaire que rien ne précède se place en premier ; un
épilogue ou une postface que rien ne suit se place en dernier ; un chapitre spécial envoyé seul dans une suite se place
après le dernier chapitre (avant un épilogue final). Plusieurs chapitres spéciaux consécutifs gardent leur ordre. Les
chapitres qui suivent un chapitre inséré sont marqués pour une nouvelle vérification du contexte.
Correspondance. Un chapitre envoyé de nouveau est retrouvé par external_id, puis par type, numéro et partie (un
chapitre 12 isolé et sa partie 1 sont le même chapitre), puis, pour un chapitre spécial sans numéro, par type et
libellé ou titre. Les documents écrits avant la carte des chapitres continuent de fonctionner : un chapitre spécial détecté d’après son
titre garde l’ordre que lui donnait son number (un prologue envoyé comme 0, un interlude comme 5.5), et un
document sans les nouveaux champs est stocké, et dédoublonné, comme avant.
Rapport. Le document d’état donne pour chaque chapitre son kind, son number, sa part, son part_count, son label,
son display_label (le libellé dans la langue source), sa position de lecture et son mapping (confidence,
reason, et detected quand il a été déduit du titre) ; le rapport de fin liste la même carte
dans chapter_map.
Modes d’analyse et fils
Avant de traduire, Libris analyse le volume : les personnages et leurs noms, les relations, les termes, les résumés de chapitre, puis la Book Bible. La traduction ne commence jamais avant que cette analyse soit terminée.
parallel(par défaut) : chaque passage est d’abord analysé isolément,threadsà la fois ; les résultats sont consolidés dans l’ordre du livre, puis chaque passage est relu de nouveau, en parallèle, à la lumière de ce qu’ont établi les passages qui le précèdent (à qui renvoie un surnom ou un pronom, quels noms désignent la même personne). Rien de ce que révèle un passage ultérieur n’est jamais montré à un passage antérieur. Un volume numéroté d’une série attend aussi, avant cette relecture, qu’un volume antérieur en cours d’analyse au même moment ait terminé son analyse (le travail est alorswaitingavecstop_reason: earlier_volume, etqueue.reasonvautearlier_volume). Cette attente dure au plusAPI_REQUEST_STALL_MINUTES; au-delà, le volume continue avec la mémoire de série disponible. Ce mode fait environ 1,8 fois plus d’appels d’analyse que le mode strict, et se termine plusieurs fois plus tôt (voir architecture (en anglais)).strict: un passage après l’autre, chacun lisant la mémoire laissée par les précédents.
threads limite le nombre de passages du volume en cours de traitement simultanément, pour l’analyse comme pour la
traduction. Sans lui, un volume utilise la capacité du provider (Livres simultanés), partagée à parts égales
entre les livres qui s’y exécutent, et jamais davantage : une valeur plus élevée est ignorée. Quand un provider répond
429 ou est surchargé, le travail attend, puis reprend à la moitié de sa largeur et s’élargit de nouveau d’un passage
par minute. Près d’un budget de coût, les appels en cours sont comptés avant de démarrer, et
le volume se restreint à un appel à la fois.
Options des envois de fichiers
Pour les envois EPUB et TXT, les options sont des champs de formulaire (ou des paramètres de requête pour un corps EPUB brut). Les valeurs vides comptent comme « non fournies » ; les options inconnues sont refusées.
| Option | Signification |
|---|---|
series ou series_id | La série par son nom (créée si elle n’existe pas) ou par son id ; pas les deux. Obligatoire pour du TXT, facultatif pour un EPUB (volume isolé sinon). |
volume | Numéro de volume (1–10000), ou latest pour le dernier volume de la série (TXT seulement, voir volume.latest plus haut). Obligatoire pour du TXT. |
external_id | Votre identifiant de la demande. |
volume_external_id | Votre identifiant du volume. |
title, author | Titre et auteur du volume (sinon, un EPUB garde les siens). |
source_language, target_language | Balises BCP 47. Toutes deux obligatoires pour du TXT. |
provider_id, quality, context_backend, final_review, priority, analysis_mode, threads | Comme dans pipeline plus haut. |
start | true (par défaut) exécute tout le pipeline ; false se contente d’importer. |
output_format | epub (entrée EPUB seulement ; la valeur par défaut pour un EPUB), json, txt, txt-zip ou epub-bilingual. |
callback_url | Voir Webhooks. |
callback_events | Événements supplémentaires séparés par des virgules, par exemple chapters.translated (voir callback_events plus haut). |
replace_changed_chapters, discard_human | TXT et DOCX seulement, comme dans le document JSON. |
split | TXT et DOCX seulement : headings découpe un fichier à ses titres de chapitre (détails) ; none (par défaut) garde chaque fichier comme un seul chapitre. |
filename | Corps EPUB brut seulement : le nom du fichier, utilisé pour deviner le numéro de volume. |
Envoyer deux fois la même demande
Envoyez un en-tête Idempotency-Key (1 à 200 caractères imprimables), un external_id, ou les deux. Renvoyer le
même contenu avec la même clé ou le même external_id répond 200 OK avec la demande d’origine et
l’en-tête Idempotent-Replayed: true : rien n’est créé deux fois. La même clé ou le même external_id avec
un contenu différent répond 409 idempotency_conflict.
Le « même contenu » est comparé après validation : l’ordre des clés et les espaces n’ont pas d’importance, et un document
JSON et l’envoi multipart des mêmes chapitres sont équivalents. priority n’en fait jamais partie. Pour
un EPUB, le contenu est le fichier plus ses options, sauf callback_url, callback_events, priority et
split.
Même sans clé, envoyer des chapitres déjà présents dans le volume avec le même texte ne
duplique jamais une série, un volume ou un chapitre : ils sont signalés comme unchanged.
Suivre une série dans la durée
Un webnovel se traduit au rythme de sa publication : envoyez chaque nouveau lot de chapitres dans sa propre demande, vers
la même série et le même volume (ou avec volume.latest: true, volume=latest pour des fichiers TXT, pour suivre
le dernier volume de la série sans tenir le compte de son numéro).
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests" \
-H "Authorization: Bearer $LIBRIS_TOKEN" \
-F "files=@Chapter 51.txt" -F "files=@Chapter 52.txt" \
-F series="Web Saga" -F volume=latest \
-F source_language=en -F target_language=fr \
-F callback_url=https://hooks.example.org/libris -F callback_events=chapters.translated
- Ajoutés dans l’ordre. Les chapitres sont placés par numéro et par partie parmi les chapitres déjà présents dans le
volume, les chapitres spéciaux après le chapitre qu’ils suivent ou là où
afterles place (voir Chapitres irréguliers), et mis en correspondance parexternal_id, puis par type, numéro et partie : un chapitre renvoyé avec le même texte estunchanged, un chapitre avec un autre texte est refusé, sauf sireplace_changed_chaptersest vrai. Mettez les numéros dans les noms de fichier (Chapter 51.txt) : un fichier sans numéro n’est numéroté qu’au sein de sa propre demande. - Lignes du site source. Une demande n’a pas d’aperçu : elle ne retire que les lignes que le volume laisse déjà de côté, enregistrées lors d’un import par l’assistant avec Exclure ces lignes coché. Elles sont retirées des cinq premiers et des cinq derniers paragraphes de chaque chapitre envoyé, avant que ses mots soient comptés ; les mêmes mots ailleurs dans un chapitre sont gardés, et un volume créé par l’API n’en enregistre aucune.
- Seuls les nouveaux chapitres sont traduits. Les chapitres déjà traduits ne sont ni retraduits ni
relus de nouveau : ils apportent leur contexte (glossaire, personnages, résumés et passages
précédents) aux nouveaux. Le travail de la demande couvre les chapitres nouveaux ou remplacés, plus tout chapitre
du volume encore dépourvu de traduction. Le document d’état les liste dans
chapters.new. - Une demande à la fois par volume. Une demande envoyée pendant que le volume est occupé attend (
queued) et démarre après celle en cours. - Résultats mis à jour. Le résultat de chaque demande peut couvrir ses propres chapitres, seulement les nouveaux, ou tout le
volume (voir
scopedans Récupérer le résultat).
Suivre une demande
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID" \
-H "Authorization: Bearer $LIBRIS_TOKEN"
# Attente longue : répond dès que la demande se termine, ou au bout de 60 secondes au plus
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID?wait=60" \
-H "Authorization: Bearer $LIBRIS_TOKEN"
?wait=<secondes> retient la réponse jusqu’à la fin de la demande, dans la limite de API_RESULT_MAX_WAIT_SECONDS
(60 par défaut, 600 au plus) ; une valeur plus grande est ramenée à cette limite, et les valeurs supérieures à 600 sont refusées
avec 422. Rien n’est maintenu ouvert dans la base de données pendant l’attente.
Comment une demande progresse
- En file d’attente. Si un autre travail est en cours sur le volume, la demande attend à l’état
queued. Ses chapitres ne sont importés qu’une fois qu’aucun travail n’est actif sur le volume. Le worker vérifie les demandes en file d’attente toutes les 2 secondes et démarre chacune dès que son volume est libre ; les demandes portant sur le même volume démarrent dans l’ordre d’arrivée. Un volume retenu par un travail en pause ou bloqué reste occupé jusqu’à ce que ce travail soit repris et se termine, ou soit annulé. - En cours. Le pipeline s’exécute sous le pilote automatique : les refus, les réponses de modèle invalides et les points de relecture ouverts n’attendent jamais une personne, et une panne de provider bascule vers des providers de repli après une attente bornée. (Si un administrateur a désactivé le pilote automatique, ou si le volume s’en est exclu, ces étapes se déroulent comme dans l’interface.)
- Finalisation. Le travail est terminé ; Libris construit et stocke le résultat, puis rédige le rapport.
- Terminée. L’un des états finaux ci-dessous.
Tout est conservé dans la base de données : redémarrer l’API ou le worker ne fait rien perdre.
| État | Signification |
|---|---|
queued | En attente que le volume soit libre. |
imported | Chapitres importés, rien n’a démarré (start valait false). C’est un état final. |
pending, running | Le travail attend un worker, ou est en cours. |
paused | Mis en pause par vous ou par une personne dans l’interface. |
waiting | Le provider est temporairement indisponible, nouvelle tentative automatique ; ou, avec stop_reason: earlier_volume, l’analyse attend un volume antérieur de la série. |
blocked | Demande une intervention, par exemple quand le provider refuse ses identifiants. |
finalizing | Le travail est terminé ; le résultat est en cours de construction. |
completed | Tous les passages sont traduits ; le résultat est stocké. |
completed_with_residuals | Le résultat est stocké, mais certains passages ont gardé leur texte source (ou le modèle les a rendus dans ce texte) ou le pilote a laissé des points ou contrôles non résolus. Les originaux sont listés dans report.residuals ; les contrôles restants sont comptés dans report.autopilot. |
failed | Voir error : le travail a échoué, l’EPUB n’a pas pu être réparé, le travail est resté bloqué trop longtemps, ou la demande a dépassé sa durée maximale. |
cancelled | Annulée par vous ou par une personne. |
Aucune demande ne tourne indéfiniment. Si son travail reste paused, blocked ou waiting plus de
API_REQUEST_STALL_MINUTES (360) minutes, Libris annule le travail et la demande échoue avec la raison. Un travail
waiting qui attend un volume antérieur (earlier_volume) en est exempté : cette attente a sa propre limite (voir
Modes d’analyse). Il en va de
même pour une demande toujours inachevée API_REQUEST_MAX_HOURS (168) heures après sa création. Quand
le pilote automatique signale son échec, la demande échoue avec la raison donnée par le pilote automatique.
Le document d’état
{
"request_id": "5b1c…", "external_id": "tbate-volume-12", "series_id": "…", "project_id": "…",
"job_id": "…", "input": "json", "status": "running",
"status_url": "/api/v1/translation-requests/5b1c…",
"result_url": "/api/v1/translation-requests/5b1c…/result",
"created_at": 1790000000.0, "updated_at": 1790000100.0, "finished_at": null,
"stage": "translation", "step": "translation",
"progress": {"segments": 412, "translated": 180, "percent": 44,
"stages": [{"key": "translation", "done": 180, "total": 412, "percent": 44}],
"analysis": null},
"estimate": {"…": "…"},
"error": "", "stop_reason": "", "next_attempt": 0,
"chapters": {"created": 3, "unchanged": 0, "replaced": 0, "new": ["…"],
"items": [{"chapter_id": "…", "external_id": "chapter-001", "number": 1, "kind": "chapter",
"part": null, "part_count": null, "label": "", "display_label": "Chapter 1",
"position": 0, "mapping": {"confidence": "high", "reason": "…", "detected": false},
"title": "Chapter 1",
"segments": 140, "translated": 60, "validated": 0, "flagged": 0, "complete": false}]},
"options": {"start": true, "final_review": true, "output_format": "json", "analysis_mode": null, "threads": null},
"priority": "normal",
"queue": null,
"result": null,
"report": null,
"webhook": {"state": "pending", "attempts": 0, "error": ""},
"chapter_events": null
}
| Champ | Signification |
|---|---|
stage | Étape actuelle du volume : import, analysis, translation, review ou export (null sans travail). |
step | Étape actuelle du travail (par exemple translation, final_review, autopilot, arbitration). |
progress | segments, translated et percent pour les chapitres de la demande (tous les chapitres pour un EPUB), et stages, la progression du volume par étape. Pendant l’analyse du volume, analysis indique où elle en est : step (extraction, consolidation, reconciliation, memory, puis book_bible en mode parallèle ; chapter_analysis, puis book_bible en mode strict), current/total passages ou synthèses, level/levels de l’arborescence de la Book Bible, et percent de l’ensemble de l’analyse ; null sinon. |
estimate | Temps et coût restants, une fois observés suffisamment d’appels au modèle ; sinon null. |
error, stop_reason, next_attempt | Pourquoi le travail s’est arrêté ou attend, et quand il fera une nouvelle tentative (heure Unix, 0 s’il n’attend pas). |
priority | La priorité de la demande (low, normal, high), telle que modifiée par une personne dans l’interface le cas échéant. |
queue | Tant que la demande attend de démarrer : position (sa place dans la file de son provider, 1 = la prochaine), reason (starting, provider_busy, account_limit, token_limit, retry_scheduled, earlier_volume pendant que l’analyse attend un volume antérieur de la série, provider_missing, ou volume_busy pendant qu’un autre travail retient le volume), effective_priority (relevée par l’attente) et next_attempt. position vaut null pour retry_scheduled, earlier_volume et volume_busy ; avec volume_busy, l’objet ne contient que position et reason ; reason vaut null pour une demande en file d’attente qui ne démarrera pas (start: false). queue lui-même vaut null dès que le travail s’exécute ou que la demande est terminée. |
chapters | Combien de chapitres ont été created, unchanged ou replaced, new (les id des chapitres créés et remplacés), et pour chaque chapitre, dans l’ordre de lecture, sa carte (kind, number, part, part_count, label, display_label, position, mapping), ses nombres de passages, de passages traduits, validés et signalés, et s’il est complete. |
result | Une fois stocké : format, media_type, filename, size, sha256, created_at. |
report | Le rapport de fin, une fois la demande terminée. |
webhook | Seulement quand un callback_url a été fourni : state (pending, delivered, failed), attempts, dernière error. |
chapter_events | Seulement quand callback_events a été fourni : batches mis en file jusqu’ici, combien sont delivered, pending ou failed, waiting_chapters (pas encore traduits) et la dernière error. |
Les heures sont des horodatages Unix en secondes.
Mettre en pause, reprendre ou annuler
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/pause" -H "Authorization: Bearer $LIBRIS_TOKEN"
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/resume" -H "Authorization: Bearer $LIBRIS_TOKEN"
curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/cancel" -H "Authorization: Bearer $LIBRIS_TOKEN"
Chacune répond avec le document d’état. Elles suivent les mêmes règles que l’interface : 409 quand
l’état du travail ne permet pas l’action. Une demande sans travail ne peut être annulée que tant qu’elle est
queued ; toute autre action sur une demande sans travail (mettre en pause ou reprendre une demande en file d’attente, ou
annuler une demande imported) répond 409 not_started. La reprise peut aussi répondre 429 queue_full (le
quota d’attente) ou 409 budget_exceeded (un budget toujours atteint).
Récupérer le résultat
# Le format par défaut : l’EPUB traduit pour un EPUB, sinon le format de sortie de la demande
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/result" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -o book.fr.epub
# JSON
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/result?format=json" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -o result.json
# Un seul fichier texte UTF-8, les chapitres sous leurs titres traduits
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/result?format=txt" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -o volume-12.txt
# ZIP : chapters/001 - Title.txt …, manifest.json avec le SHA-256 de chaque fichier
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/result?format=txt-zip" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -o volume-12.zip
# EPUB bilingue pour la relecture : chaque paragraphe source avec sa traduction (quelle que soit l’entrée)
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/result?format=epub-bilingual&layout=side-by-side" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -o volume-12-bilingual.epub
# Ce qui est prêt jusqu’ici
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/result?format=json&partial=true" \
-H "Authorization: Bearer $LIBRIS_TOKEN"
Choisir le format. ?format= l’emporte (epub, json, txt, txt-zip, epub-bilingual) ;
sinon l’en-tête Accept (application/epub+zip, application/json, text/plain,
application/zip) ; sinon le format propre à la demande. epub n’existe que pour une demande qui a envoyé un
EPUB (409 format_unavailable sinon).
EPUB bilingue. epub-bilingual existe pour toute demande, quel que soit ce qui a été envoyé : un nouvel EPUB 3 avec
une page par chapitre, où chaque paragraphe source est suivi de sa traduction (layout=interleaved,
par défaut) ou placé à côté d’elle sur deux colonnes (layout=side-by-side ; les colonnes s’empilent sur un
écran étroit). Il ne contient que le texte (ni image, ni mise en forme d’origine) et sert à la relecture sur une
liseuse. Dans un résultat partiel, un passage sans traduction affiche sa source et une
traduction vide marquée —. Un résultat bilingue stocké est la version entrelacée ; layout=side-by-side est
produit à la demande. Quand EPUBCheck est installé et refuse le livre, la réponse est
422 delivery_failed.
Ce qu’il couvre. ?scope= choisit, dans l’ordre de lecture :
scope | Chapitres |
|---|---|
request (par défaut) | Les chapitres envoyés par la demande, y compris ceux qui sont unchanged ; pour un EPUB, le livre entier. |
new | Seulement les chapitres que la demande a créés ou remplacés : les nouveaux chapitres d’une suite. |
volume | Tous les chapitres du volume, y compris ceux des demandes précédentes : le volume mis à jour. |
Le format epub couvre toujours le livre entier. Le résultat JSON indique son scope. Avec new ou volume,
complete (et X-Libris-Complete) ne vaut true que si tous les chapitres couverts sont entièrement traduits
et que le pilote automatique ne signale aucun point ou contrôle non résolu.
Résultats stockés et résultats produits à la demande. Quand une demande se termine avec succès, Libris construit son résultat une fois, dans
le format par défaut de la demande et le périmètre request, et le stocke sous DATA_DIR/results/<request id>/. Ce fichier est
servi tel quel. Les autres formats sont produits à la demande à partir de la base de données. Les fichiers stockés sont supprimés au bout de
RETENTION_RESULTS_DAYS (30 jours) ; une nouvelle demande produit alors le résultat à partir de la base de données.
Avant la fin. Tant que la demande n’est pas terminée, la réponse est 409 result_not_ready avec
status, incomplete_chapters et un en-tête Retry-After: 5. Une demande en échec ou annulée répond
409 request_failed ou 409 request_cancelled avec la raison (reason). Ajoutez ?wait=<secondes> pour attendre
d’abord la fin, ou ?partial=true pour obtenir ce qui est prêt : complete vaut alors false, et
incomplete_chapters liste les chapitres qui ne sont pas entièrement traduits.
Les passages manquants gardent leur texte source dans tous les formats : les résidus d’une demande
completed_with_residuals, et les passages pas encore traduits dans un résultat partiel.
Chaque résultat porte deux en-têtes : X-Libris-Complete: true|false et X-Libris-Status: <status>.
Les fichiers autres que JSON portent aussi un Content-Disposition avec un nom de fichier. Si le volume a été supprimé,
la réponse est 404 volume_not_found.
L’EPUB livré
L’EPUB traduit est reconstruit à partir du fichier d’origine avec chaque passage traduit. Les passages sans
traduction utilisable gardent leur texte source et leur balisage : les passages non traduits, les passages conservés dans
l’original par le pilote automatique, et les passages dont le balisage traduit ne correspond plus à celui de la source
(markup_mismatch).
Le livre est ensuite vérifié par EPUBCheck (quand le serveur dispose de EPUBCHECK_JAR). Si EPUBCheck le rejette,
Libris le répare de lui-même : les fichiers désignés par les erreurs reviennent à leur texte source (les passages
concernés deviennent des résidus avec la raison epubcheck_repair), et le livre est vérifié de nouveau, jusqu’à
DELIVERY_REPAIR_ATTEMPTS (3) fois. Les erreurs que l’EPUB d’origine contenait déjà ne sont pas dues à la
traduction : elles sont listées dans report.delivery.inherited_errors et ne bloquent pas la livraison. Ce n’est que
lorsque les réparations sont épuisées que la demande échoue, avec les erreurs dans report.delivery.errors. Quand
l’EPUB est produit à la demande et ne peut pas être construit, la réponse est 422 delivery_failed.
Le résultat JSON
{
"schema_version": 1,
"request_id": "…", "external_id": "tbate-volume-12", "status": "completed", "complete": true,
"scope": "request",
"series": {"id": "…", "name": "The Synthetic Saga"},
"volume": {"project_id": "…", "external_id": "volume-12", "number": 12, "label": null, "title": "Volume 12"},
"source_language": "en", "target_language": "fr",
"strategy": {"provider": {"name": "Local", "model": "…"}, "quality": "high",
"context_backend": "hybrid", "final_review": true},
"incomplete_chapters": [],
"chapters": [{
"chapter_id": "…", "external_id": "chapter-001", "number": 1,
"kind": "chapter", "part": null, "part_count": null, "label": "",
"display_label": "Chapitre 1", "position": 0,
"title": "Chapter 1", "translated_title": "Chapitre 1",
"complete": true, "missing_segments": 0,
"translation": "Premier paragraphe.\n\nDeuxième paragraphe.\n",
"source_sha256": "…", "sha256": "…",
"review": {"segments": 2, "validated": 0, "flagged": 0},
"issues": [], "flagged_passages": []
}],
"report": {"…": "the completion report"}
}
sha256est le SHA-256 detranslation(UTF-8) ;source_sha256est celui du texte source normalisé.- Les chapitres sont listés dans l’ordre de lecture (
position), une entrée par chapitre stocké : les parties d’un chapitre restent des entrées distinctes, chacune avec sonexternal_id.display_labelest le libellé du chapitre dans la langue cible (Chapitre 12 (partie 2),Prologue,Interlude – Ayla) ; les résultats TXT, ZIP et EPUB bilingue l’utilisent comme titre d’un chapitre dont le titre ne donne que son numéro ou son type, et le ZIP d’un volume comportant des parties ou des chapitres spéciaux numérote ses fichiers dans l’ordre de lecture. issuesliste les problèmes de qualité non résolus (segment_id,severity,code,message) ;flagged_passagesliste les passages toujours signalés (check,errorourefused, non validés).strategyne nomme que le provider et le modèle, jamais l’adresse ni la clé du provider.reportest le rapport de fin une fois la demande terminée,nullavant.
Rapport de fin
Le rapport figure dans le document d’état (report), dans le résultat JSON, sous le nom report.json dans
le ZIP stocké, et dans le webhook.
{
"version": 1, "outcome": "completed_with_residuals", "reason": null,
"passages": {"total": 412, "translated": 410, "source_retained": 1, "untranslated": 1, "flagged": 3,
"validated": 0, "human": 0,
"by_status": {"ok": 407, "check": 3, "source_retained": 1, "error": 1}},
"residual_total": 2,
"residuals": [{"segment_id": "…", "chapter_id": "…", "chapter_external_id": null,
"chapter_title": "…", "position": 118, "status": "source_retained",
"kept": "source", "reason": "…"}],
"residuals_truncated": false,
"usage": {"calls": 1290, "prompt_tokens": 2410000, "completion_tokens": 610000,
"cached_calls": 12, "cost": 3.41},
"cost": {"estimated": 3.9, "actual": 3.41, "budget": 5.0, "book_spent": 4.62, "warning": null,
"paused_for_budget": false, "provider_switches": 0},
"durations": {"total_seconds": 5230.1, "queued_seconds": 0.4, "job_seconds": 5211.8},
"autopilot": {"outcome": "completed_with_residuals", "rounds": 2, "reason": null},
"decisions": {"autopilot": 17,
"intake": [{"file": 2, "name": "notes.txt", "chapter_number": 3.0,
"confidence": "low", "reason": "…"}]},
"chapter_map": [{"chapter_id": "…", "external_id": null, "status": "created", "kind": "afterword",
"number": null, "part": null, "part_count": null, "label": "", "position": 2,
"confidence": "high", "reason": "…"}],
"delivery": {"validation": {"available": true, "valid": true}, "repairs": [], "inherited_errors": []},
"quality": {"scored": 411, "average": 91.4, "minimum": 40, "to_review": 6, "review_below": 70,
"bands": {"good": 380, "fair": 25, "weak": 5, "poor": 1},
"histogram": [0, 0, 0, 0, 1, 2, 3, 10, 35, 360],
"weakest_chapters": [{"chapter_id": "…", "title": "…", "external_id": null, "number": 12.0,
"project_id": "…", "passages": 38, "scored": 38, "average": 78.2,
"minimum": 40, "weak": 3, "…": "…"}],
"review_first": [{"segment_id": "…", "chapter_id": "…", "chapter_title": "…",
"chapter_external_id": null, "position": 118, "score": 40, "band": "poor",
"signals": [{"code": "source_retained", "count": 1, "penalty": 60}],
"excerpt": "…", "…": "…"}]}
}
| Champ | Signification |
|---|---|
outcome, reason | completed, completed_with_residuals, failed ou cancelled, et la raison quand la demande n’a pas abouti. |
passages | Décomptes portant sur les passages de la demande (le livre entier pour un EPUB). |
residuals | Passages livrés dans leur texte source, 500 au plus (residual_total les compte tous, residuals_truncated indique si la liste est tronquée). Ils comprennent un passage dont la traduction est encore le texte source (une alerte unchanged ou untranslated ouverte que son texte mérite encore), avec les mots du contrôle pour raison. reason est celle du pilote automatique quand il en a consigné une, sinon la dernière erreur du passage, sinon source_retained, untranslated, markup_mismatch ou epubcheck_repair. |
usage | Appels au modèle du travail de la demande et leurs jetons. cost ne compte que les appels dont le prix est connu, et vaut null quand aucun n’en avait. |
cost | L’estimation faite au démarrage du travail (null s’il n’y en a pas eu) face à son coût réel (usage.cost), le budget du livre (null s’il n’y en a pas), ce que le livre a coûté au total, l’avertissement donné au lancement quand l’estimation dépassait ce qui restait, si le travail a été mis en pause à cause d’un budget, et combien de fois il est passé à un provider moins cher pour cette raison. |
durations | Secondes écoulées depuis la création de la demande, passées à attendre le volume, et passées dans le travail. |
autopilot | Comment le pilote automatique s’est terminé (null quand il ne s’est pas exécuté). |
decisions | autopilot : le nombre de décisions que le pilote automatique a consignées pour le travail ; intake : les choix faits à la lecture de l’envoi (numéros de volume et de chapitre, places des chapitres spéciaux, encodage du texte, EPUB réutilisé). |
chapter_map | Chaque chapitre de la demande : chapter_id, external_id, status (created, unchanged, replaced), kind, number, part, part_count, label, position de lecture à l’import, ainsi que la confidence et la reason de la carte (fournie ou détectée). |
quality | Scores de qualité (0–100) des passages traduits de la demande, calculés à partir des signaux que Libris enregistre (vérifications, critiques, doutes, appels en échec, récupérations, originaux conservés ; voir l’architecture (en anglais)) : nombre, moyenne, minimum, bands (good à partir de 85, fair à partir de 70, weak à partir de 50, poor en dessous), un histogram en dix tranches, to_review (sous review_below et non validés par une personne), les 10 chapitres les plus faibles et les 10 passages à relire en priorité, avec les signaux qui les ont fait baisser. null quand la demande n’a pas de volume. |
delivery | EPUB seulement : la validation EPUBCheck, les réparations effectuées (repairs : tentative, erreurs, fichiers, passages restaurés), inherited_errors, et errors quand la livraison a échoué. |
Le journal complet des décisions du pilote automatique pour un livre s’affiche dans l’interface (onglet Pilote automatique du livre) ; voir le guide du pilote automatique.
Webhooks
Une requête peut indiquer une callback_url. Quand elle se termine (quel que soit son statut final, et
aussi à imported), le worker envoie un POST à cette URL ; avec callback_events, il en envoie
aussi un auparavant pour chaque lot de chapitres traduits. Le webhook
est une commodité : le document de statut reste la référence, et vous pouvez toujours l’interroger.
Activer les webhooks (administrateurs)
Les webhooks sont désactivés tant qu’un administrateur n’a pas autorisé au moins un hôte. Dans Paramètres › API d’automatisation › Webhooks des requêtes d’API, ou avec des variables d’environnement :
| Réglage | Variable d’environnement | Valeur par défaut |
|---|---|---|
Hôtes autorisés (hooks.example.org, *.partner.example pour ses sous-domaines) | API_WEBHOOK_HOSTS (séparés par des virgules) | vide : webhooks refusés |
| Réseaux privés autorisés, en notation CIDR | API_WEBHOOK_PRIVATE_NETWORKS | vide |
| Nombre maximal de tentatives | API_WEBHOOK_MAX_ATTEMPTS | 6 |
| Délai d’expiration d’un appel, en secondes | API_WEBHOOK_TIMEOUT_SECONDS | 10 |
| Secret de signature global (32 caractères au moins) | API_WEBHOOK_SECRET | vide |
Les valeurs enregistrées dans l’interface priment sur l’environnement jusqu’à un clic sur Revenir aux valeurs de l’environnement. Elles s’appliquent sans redémarrage. Chaque webhook doit être signé : un jeton doit avoir son propre secret de signature (choisi à la création du jeton), ou bien un secret global doit exister.
Ce qui est envoyé
{
"event": "translation_request.finished",
"request_id": "…", "external_id": "…",
"status": "completed_with_residuals", "error": null,
"project_id": "…", "job_id": "…",
"status_url": "/api/v1/translation-requests/…",
"result_url": "/api/v1/translation-requests/…/result",
"artifact": {"format": "epub", "size": 812345, "sha256": "…"},
"report": {"outcome": "completed_with_residuals", "residual_total": 2, "…": "…"},
"finished_at": 1790000000.0
}
| En-tête | Valeur |
|---|---|
X-Libris-Event | translation_request.finished |
X-Libris-Delivery | <identifiant de la requête>:<numéro de la tentative> |
X-Libris-Timestamp | Heure Unix en secondes |
X-Libris-Signature | sha256=<hex> : HMAC-SHA256 de <timestamp>.<body> |
User-Agent | Libris-Webhook/1 |
La signature utilise le secret de webhook propre au jeton s’il en a un, sinon le secret global.
Vérifier un webhook
Vérifiez la signature sur le corps brut, et refusez les horodatages trop anciens pour bloquer les rejeux
(le client d’exemple fait de même dans verify_signature et le met en œuvre avec sa commande webhooks) :
import hashlib
import hmac
import time
def verify(secret: str, body: bytes, timestamp: str, signature: str, tolerance: int = 300) -> bool:
if abs(time.time() - int(timestamp)) > tolerance:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, "sha256=" + expected)
Nouvelles tentatives
Toute réponse 2xx vaut remise. Tout le reste (un autre statut, un délai dépassé, une erreur réseau)
donne lieu à une nouvelle tentative avec un délai exponentiel : 30 secondes, puis 60, 120… jusqu’à une
heure entre deux tentatives, pour au plus API_WEBHOOK_MAX_ATTEMPTS appels. Les redirections ne sont
pas suivies. Le document de statut indique l’état (state) du webhook, le nombre de tentatives
(attempts) et la dernière erreur (error).
Protections
Une callback_url qui enfreint l’une de ces règles est refusée immédiatement avec 422 callback_refused,
avant que quoi que ce soit ne soit enregistré :
- ce doit être une URL
httpouhttpssans nom d’utilisateur ni mot de passe, de 2000 caractères au plus ; - son hôte doit figurer dans la liste des hôtes autorisés ;
- un secret de signature doit exister (celui du jeton ou le secret global) ;
- son nom ne doit se résoudre qu’en adresses publiques. Les adresses de bouclage, privées, lien-local, réservées et multicast sont refusées, sauf si elles appartiennent à un réseau privé autorisé.
Le nom est résolu à nouveau avant chaque appel, et l’appel part vers l’adresse qui vient d’être vérifiée
(avec le nom d’origine dans l’en-tête Host et comme nom de serveur TLS), si bien qu’une réponse DNS qui
changerait entre-temps ne peut pas le détourner. Les variables d’environnement de proxy sont ignorées.
Lots de chapitres traduits
Une requête dont les callback_events contiennent chapters.translated reçoit aussi, pendant le
travail, un webhook par lot de chapitres dont tous les passages ont une traduction : un client peut
ainsi publier les chapitres lot par lot au lieu d’attendre la requête entière. Les chapitres déjà
traduits au moment où la requête a été importée ne sont pas annoncés. Le webhook final
translation_request.finished de la requête est envoyé comme d’habitude.
{
"event": "chapters.translated",
"request_id": "…", "external_id": "…", "series_id": "…", "project_id": "…",
"batch": 2,
"chapters": [{"chapter_id": "…", "external_id": "chapter-052", "number": 52, "title": "Chapter 52"}],
"announced": 2, "total": 3,
"status_url": "/api/v1/translation-requests/…",
"result_url": "/api/v1/translation-requests/…/result?partial=true",
"created_at": 1790000000.0
}
batch compte à partir de 1 pour chaque requête ; announced est le nombre de chapitres annoncés
jusqu’ici, total le nombre de chapitres de la requête. Les lots utilisent les mêmes hôtes autorisés, la
même signature, les mêmes en-têtes et les mêmes nouvelles tentatives que le webhook final, avec
X-Libris-Event: chapters.translated et X-Libris-Delivery: <request id>:chapters.translated:<batch>:<attempt>. Ils sont envoyés avant le webhook final quand les
deux sont dus, mais un lot qui fait l’objet d’une nouvelle tentative peut arriver après lui : triez-les
par batch. Un lot est un brouillon : tant que la requête n’est pas terminée, la relecture finale peut
encore améliorer ses chapitres. Récupérez-les avec ?partial=true&scope=new (ou format=json, qui
donne chaque chapitre avec son chapter_id) ; le résultat final reste la référence.
Lister vos séries
curl -sS "$LIBRIS_URL/api/v1/series" -H "Authorization: Bearer $LIBRIS_TOKEN"
curl -sS "$LIBRIS_URL/api/v1/series/$SERIES_ID" -H "Authorization: Bearer $LIBRIS_TOKEN"
La liste est triée par nom et donne id, name, kind, source_language, target_language, archived, volumes, created_at, updated_at. Le détail ajoute volume_list, dans l’ordre de lecture de la série, avec
project_id, title, volume_number, volume_label, external_id, source_format, project_kind, status, chapters. Seules les séries dont le propriétaire du jeton est propriétaire sont visibles ; les volumes
partagés avec lui ne le sont pas.
Un volume spécial (« DX1 », « EX », « LN 14+ ») n’a pas de volume_number ; une personne peut lui donner
à la place une étiquette libre volume_label (le volume.label du résultat). Une étiquette qui commence
par un nombre (14+, LN 14++, Vol. 3.5) place le volume juste après ce numéro ; toute autre étiquette
le place en fin de série ; un volume qui n’a ni l’un ni l’autre reste hors de l’ordre de lecture. Cet ordre
décide des volumes qu’un volume attend et de la mémoire de série qu’il reçoit.
Glossaires partagés
Un glossaire partagé est la terminologie d’un univers commun à plusieurs de vos séries (lieux, titres, sorts…). Chaque série en suit un au plus ; ses termes acceptés s’appliquent à chaque volume de la série, après les termes propres au livre et à la série : livre > série > glossaire partagé. Un terme partagé verrouillé est imposé et vérifié dans chaque passage, et l’emporte sur un terme non verrouillé proposé par un volume précédent ; une décision de série prise par une personne ou une dérogation délibérée d’un volume l’emporte toujours. Les mêmes glossaires se gèrent dans l’interface (Glossaires partagés).
# En créer un ; les langues sont facultatives (si elles sont indiquées, il ne s’applique qu’aux volumes de la même paire).
curl -sS "$LIBRIS_URL/api/v1/glossaries" -H "Authorization: Bearer $LIBRIS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Glass Road universe", "source_language": "en", "target_language": "fr"}'
# Prévisualiser un import, puis l’appliquer.
curl -sS "$LIBRIS_URL/api/v1/glossaries/$GLOSSARY_ID/import?dry_run=true" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -F file=@universe.csv -F strategy=replace
curl -sS "$LIBRIS_URL/api/v1/glossaries/$GLOSSARY_ID/import" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -F file=@universe.csv -F strategy=replace
# Faire suivre ce glossaire par une série (envoyez {"glossary_id": null} pour l’en détacher).
curl -sS -X PUT "$LIBRIS_URL/api/v1/series/$SERIES_ID/shared-glossary" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -H "Content-Type: application/json" \
-d "{\"glossary_id\": \"$GLOSSARY_ID\"}"
# Le télécharger ; pour les tableurs : CSV avec points-virgules et indicateur d’ordre des octets (BOM).
curl -sS "$LIBRIS_URL/api/v1/glossaries/$GLOSSARY_ID/export/csv?delimiter=semicolon&bom=true" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -o universe.csv
Un glossaire donne id, name, description, source_language, target_language, created_at, updated_at, term_count, locked_count, series (les séries qui le suivent, id et name) et, pour un glossaire donné,
terms avec id, source, translation, category, description, locked, accepted. Seuls les termes acceptés
sont appliqués.
Export. GET …/export/{format} accepte json, csv ou tbx. Pour le CSV, delimiter vaut comma
(par défaut), semicolon ou tab, et bom=true ajoute un indicateur d’ordre des octets UTF-8 (par défaut
false) ; delimiter=semicolon&bom=true s’ouvre directement dans Excel.
Fichiers. JSON (une liste de termes avec les champs ci-dessus), CSV ou TBX (v2 et v3). Les fichiers CSV
peuvent comporter un indicateur d’ordre des octets ou provenir d’Excel sous Windows ; le séparateur (;,
, ou tabulation) est détecté, et les en-têtes sont reconnus en français ou en anglais (source,
terme source, traduction, target, catégorie, notes, verrouillé, accepté…) ; un fichier sans
en-tête utilise ses deux premières colonnes. Champs de formulaire d’un import :
| Champ | Valeurs | Par défaut |
|---|---|---|
file | Le fichier du glossaire (2 Mo au plus) | obligatoire |
strategy | skip conserve les termes en place ; replace remplace les termes non verrouillés qui diffèrent ; replace_all remplace aussi les termes verrouillés | skip |
delimiter | semicolon, comma ou tab | détecté |
mapping | Objet JSON associant un champ à un numéro de colonne (à partir de 0), par exemple {"source": 0, "translation": 2} | d’après les en-têtes |
header | true ou false : la première ligne contient-elle les noms de colonnes | détecté |
skip_invalid | true écarte les lignes invalides au lieu de refuser le fichier | false |
La réponse est le rapport d’import : format, encoding, delimiter, columns (la première ligne),
header, mapping, strategy, counts (terms, new, unchanged, conflicts, replaced, kept, duplicates, errors), les listes new, conflicts (avec existing, incoming, les champs qui diffèrent fields,
locked et l’action action, replace ou keep), duplicates (une source répétée dans le fichier :
c’est la première ligne qui compte) et errors (line, message), chacune limitée à 200 éléments
(truncated), et applied. Un import appliqué ajoute imported, replaced et skipped. Les sources sont
comparées sans tenir compte de la casse. Une prévisualisation (dry_run=true) liste les lignes invalides
au lieu de refuser le fichier.
Administration des comptes (API de session)
La gestion des comptes est délibérément séparée de l’automatisation. Les routes suivantes exigent le
cookie de session authentifiée d’un administrateur et les protections de même origine de
l’interface ; un jeton porteur d’automatisation n’accorde pas ces droits. Elles se trouvent sous /api,
pas sous /api/v1, et ne font pas partie du document OpenAPI de l’automatisation. L’interface les
expose dans Paramètres › Utilisateurs.
| Méthode et chemin | Corps et résultat |
|---|---|
GET /api/users | Liste les comptes, avec notamment id, username, email, admin, active et sso_subject. Un sso_subject non vide identifie un compte lié au SSO. Les empreintes de mots de passe et les secrets d’authentification ne sont jamais renvoyés. |
GET /api/users/seats | {used, allowed, refusal} : les comptes actifs, le nombre autorisé par la licence et un message de refus ou null. allowed: 0 signifie que la licence ne fixe aucune limite de comptes. |
POST /api/users | {username, password, email} crée un compte local et renvoie sa vue avec 201. email est facultatif et vaut "" par défaut. Aucune invitation ni aucun mot de passe initial n’est envoyé par e-mail. |
PUT /api/users/{id} | Mise à jour partielle : n’importe lesquels de username, email, admin, active. Les champs omis ou null restent inchangés ; email: "" supprime l’adresse d’un compte local. Les champs inconnus sont refusés. Renvoie le compte mis à jour et révoque ses sessions. |
PUT /api/users/{id}/password | {password} réinitialise un mot de passe local, invalide les liens de récupération émis auparavant et révoque les sessions du compte. Renvoie {ok: true}. |
DELETE /api/users/{id}/second-factor | Retire le second facteur propre à Libris pour permettre la récupération du compte et révoque ses sessions. Ne change pas la politique d’authentification du provider SSO. |
DELETE /api/users/{id}?transfer_to={recipient_id} | Supprime un autre compte local après les vérifications ci-dessous ; renvoie {deleted: true}. transfer_to est obligatoire quand le compte possède des livres, des séries ou des glossaires partagés. |
Un nom d’utilisateur compte de 2 à 80 caractères : lettres, chiffres, _, ., @ ou -. Les mots de
passe comptent de 12 à 200 caractères. Une adresse e-mail compte au plus 320 caractères et est stockée
sous forme normalisée en minuscules. Une nouvelle adresse ne peut pas reprendre celle d’un autre compte
local, sans tenir compte de la casse ; une adresse vide est admise. Les doublons d’adresses déjà présents
ne sont pas réécrits automatiquement. La récupération par une ancienne adresse partagée ne choisit pas un
compte arbitraire : utilisez plutôt le nom d’utilisateur, qui est unique. Changer une adresse invalide les
liens de récupération envoyés à l’adresse précédente.
Les comptes liés au SSO conservent leur identité et leurs identifiants chez le fournisseur d’identité. Les
administrateurs locaux peuvent modifier admin et active, mais modifier username ou email,
réinitialiser un mot de passe ou supprimer le compte localement renvoie 409. Un ancien mot de passe
local ne peut pas servir de connexion alternative pour une identité liée au SSO. La création de compte
présentée ici ne lie jamais un compte local à une identité SSO par correspondance des adresses e-mail.
Les comptes liés à l’annuaire (ldap_subject non vide, voir ldap.fr.md) suivent la même règle
pour username, email, les mots de passe et la suppression, mais gardent la seconde étape de Libris : un
administrateur peut toujours retirer leurs facteurs, et ils confirment les changements de facteurs et les
preuves récentes avec leur mot de passe d’annuaire. Quand un groupe administrateur est configuré, modifier
admin sur un tel compte renvoie 409.
| Route de l’annuaire | Contrat |
|---|---|
GET /api/auth/ldap | Publique. {enabled, label} pour l’écran de connexion. |
POST /api/auth/ldap/login | {username, password}. Mêmes réponses que POST /api/auth/login (une session, ou {second_factor, token, username}) ; 401 « Identifiants incorrects. » pour un identifiant inconnu, plusieurs entrées correspondantes, un mot de passe faux ou vide ; 403 pour une personne hors des groupes autorisés, une personne inconnue quand la création de comptes est désactivée, ou un compte désactivé ; 402 seats_exceeded ; 503 quand l’annuaire est injoignable. Limitée avec la connexion locale, sous le même nom. |
GET / PUT /api/settings/ldap | Administrateur. Les réglages de configuration.fr.md ; bind_password est en écriture seule (null le conserve) et la réponse indique seulement has_bind_password. PUT exige une preuve récente et refuse ldap:// en clair sans StartTLS ou sans allow_plaintext explicite. |
POST /api/settings/ldap/test | Administrateur. {username?} : se lie avec le compte de service et, si un identifiant est donné, renvoie l’entrée trouvée par le filtre (dn, username, email). Aucun mot de passe d’utilisateur n’est essayé. |
POST /api/users/{id}/directory | Administrateur, preuve récente. {login?} (par défaut : le nom du compte) : rattache un compte local à son entrée d’annuaire, oublie son mot de passe local, ses liens de récupération et ses sessions, et garde ses livres, ses facteurs et ses jetons. 409 s’il est déjà rattaché ou si l’entrée appartient à un autre compte. |
Preuve récente pour les modifications sensibles de comptes
Toutes les modifications de comptes ci-dessus, ainsi que les changements de l’adresse e-mail de
récupération du compte local courant par PUT /api/auth/notifications, exigent une preuve datant de
300 secondes au plus, rattachée à la session courante. La simple consultation de la liste et les
préférences de notification et d’envoi n’en exigent pas. Une preuve expirée ou absente renvoie 403 avec
{detail: {code: "reauth_required", message, methods}}. Les jetons porteurs ne peuvent pas fournir cette
preuve. Les sessions existantes démarrent sans preuve ; une connexion locale qui vient d’aboutir compte si
ses facteurs requis ont été vérifiés. WebAuthn ne compte que si l’assertion signée prouve la vérification
de l’utilisateur (UV). Une connexion OIDC ordinaire n’élève jamais la session à l’insu de l’utilisateur.
| Route de session | Contrat |
|---|---|
GET /api/auth/reauth | {valid_until, methods, code_required} ; methods contient les méthodes utilisables password, key ou sso. Les facteurs d’aucun autre compte ne sont exposés. |
POST /api/auth/reauth/password | {password, code} ; le code TOTP existant ou un code de récupération à usage unique est exigé quand un second facteur est actif. Refuse les identités SSO. |
POST /api/auth/reauth/key/begin | Renvoie {token, options} pour une clé existante, avec userVerification: "required". Le défi appartient à cette session et expire au bout de cinq minutes. |
POST /api/auth/reauth/key | {token, credential} ; vérifie la signature, le RP, l’origine, l’appartenance, le compteur et l’UV, puis consomme le défi une seule fois. |
POST /api/auth/reauth/sso/begin | Renvoie {token, url} avec des state/nonce/PKCE neufs, prompt=login et max_age=0 ; utilisez une fenêtre séparée, en préservant la session et le brouillon d’origine. |
POST /api/auth/reauth/sso/complete | {token} consomme la demande vérifiée par le provider, depuis la session d’origine uniquement ; renvoie {valid_until}. |
Le rappel OIDC existant distingue une confirmation d’une connexion. Une confirmation vérifie la
signature et un auth_time numérique, fini et récent, l’émetteur, le client, le sujet et le nonce
attendus, puis marque seulement la demande comme vérifiée. Elle ne crée pas de compte et n’ouvre pas de
session, et fonctionne sans le cookie SameSite=Strict, omis lors d’une redirection intersite. La fenêtre
d’origine, de même origine, doit terminer la demande. Un refus du provider, un auth_time absent ou
périmé, une identité qui ne correspond pas, une expiration ou un rejeu refusent l’élévation ; il n’y a pas
de repli SSO silencieux. Les confirmations échouées ne relancent pas les modifications de comptes.
L’interface ne relance que la seule requête HTTP refusée, une fois au plus. Annuler conserve le brouillon. Cette politique porte sur les modifications de comptes ; elle ne prétend pas protéger chaque réglage SMTP, de licence ou de secret de provider de l’installation. Les opérations personnelles sur les facteurs et le mot de passe conservent leurs propres vérifications existantes du mot de passe ou du code.
Garde-fous de suppression et de transfert
Supprimer un compte n’est pas la même chose que le désactiver. Les comptes désactivés conservent leur bibliothèque ; la suppression retire les sessions, les jetons d’API et les liens de relecture créés par le compte qui part. Un transfert explicite préserve les livres, les séries et les glossaires partagés. L’historique des traductions conserve le texte, avec l’auteur parti anonymisé. Le destinataire doit être un autre compte actif. Un nom de série ou de glossaire partagé déjà présent dans la bibliothèque du destinataire est refusé plutôt que fusionné ou écrasé.
La suppression est aussi refusée tant que des travaux peuvent encore engager des dépenses auprès d’un
provider : pending, waiting, analyzing, translating, reviewing ou syncing. Les identifiants
de providers personnels ne sont pas transférés avec un livre : un compte dont un provider personnel a
encore une clé d’API, ou dont un provider personnel codex_chatgpt a ses identifiants OAuth dans
l’adaptateur, ne peut pas être supprimé. Lorsque le transfert est permis sans ces identifiants, les
sélections de providers personnels sont retirées des livres et séries transférés ; choisissez un provider
autorisé avant de reprendre le travail.
Avant de supprimer un tel compte, retirez définitivement ses providers personnels dans Paramètres ›
Providers LLM. POST /api/providers/{id}/retire prend {"confirm":true} (un booléen littéral),
efface la clé enregistrée, et conserve la ligne du provider ainsi que tous les coûts historiques. Un
provider Codex doit réussir à se déconnecter par son pont puis indiquer connected:false ; sinon le
retrait renvoie 502 et la suppression du compte reste refusée. Des travaux actifs, des baux de worker
non expirés et des appels de modèle en cours refusent le retrait (409). Mettez d’abord le travail en
pause et attendez qu’il soit réellement terminé.
Le retrait est irréversible ; créez un nouveau provider pour vous reconnecter plus tard. Les providers
retirés refusent la modification, les tests de modèle, la connexion Codex et l’admission d’inférence, y
compris pour une requête qui avait chargé le provider avant son retrait. La liste habituelle des providers
les exclut ; GET /api/providers?include_retired=true inclut les lignes d’historique pour leur
propriétaire ou les administrateurs. Les clés d’API stockées ailleurs ne sont pas révoquées chez le
provider externe : révoquez-les séparément si nécessaire. Ne contournez jamais un refus en supprimant
directement dans la base de données. Un retour à une version antérieure supprime le marqueur de retrait :
ne revenez pas à une application plus ancienne sur une base de données contenant des providers retirés
sans restaurer la sauvegarde d’avant la mise à jour qui lui correspond.
Les principaux refus sont :
401: aucune session valide ;403: l’appelant n’est pas administrateur.404: le compte n’existe pas.402: créer ou réactiver un compte dépasserait le nombre de comptes autorisé par la licence.409: identité locale en double, identité SSO protégée, tentative de retirer son propre accès administrateur, transfert de bibliothèque manquant ou en conflit, travail actif, ou identifiants de providers personnels.422: champs invalides, ou destinataire du transfert invalide ou inactif quand un transfert est exigé.
Un administrateur ne peut pas supprimer son propre compte par /api/users/{id}. Le parcours de départ
distinct de Mon compte demande le mot de passe actuel et maintient en place le dernier administrateur
actif.
Connexions Nextcloud / WebDAV (API de session)
Les routes de l’interface derrière Mon compte › Connexions Nextcloud / WebDAV, Depuis
Nextcloud/WebDAV dans l’assistant d’import et Envoyer vers Nextcloud/WebDAV sur un livre. Elles
exigent une session connectée (pas un jeton d’automatisation), se trouvent sous /api, pas sous
/api/v1, et n’existent que si l’administrateur a défini WEBDAV_HOSTS
(configuration.fr.md). Une connexion appartient à
son compte : un autre compte, administrateurs compris, reçoit 404.
| Méthode et chemin | Corps et résultat |
|---|---|
GET /api/webdav/connections | {enabled, hosts, connections} ; chaque connexion vaut {id, name, url, username, has_password, password_readable, automatic, automatic_folder, automatic_format, created_at, updated_at}. Le mot de passe lui-même n’est jamais renvoyé. |
POST /api/webdav/connections | {name, url, username, password, automatic, automatic_folder, automatic_format} → 201 et la connexion. url doit désigner un hôte autorisé, en HTTPS (HTTP seulement à l’intérieur de WEBDAV_PRIVATE_NETWORKS), sans identifiants, requête ni fragment. automatic (par défaut false) envoie chaque livre dont le compte est propriétaire vers automatic_folder (relatif à la connexion, par défaut sa racine) au format automatic_format (mêmes valeurs qu’un envoi, par défaut epub) à la fin de sa traduction ou de sa révision. |
PUT /api/webdav/connections/{id} | Même corps ; un password absent conserve celui enregistré, sauf si le serveur ou le nom d’utilisateur change (422 : saisissez-le à nouveau). |
DELETE /api/webdav/connections/{id} | Supprime la connexion et ses envois en attente. |
POST /api/webdav/connections/{id}/test | PROPFIND (profondeur 0) de l’adresse : {ok: true}, ou le refus. |
GET /api/webdav/connections/{id}/browse?path=Livres | {path, parent, entries: [{name, path, folder, size, modified}]} : les enfants directs d’un dossier, dossiers en premier. Les chemins sont relatifs à l’adresse de la connexion et ne remontent jamais au-dessus. |
POST /api/imports/{session_id}/webdav | {connection_id, path} : lit le fichier (au plus MAX_UPLOAD_MB) dans la session d’import, avec les mêmes vérifications et la même réponse que POST /api/imports/{session_id}/files. Un fichier d’un autre format est refusé avant d’être téléchargé. |
GET /api/projects/{id}/webdav-publications | Les envois de ce volume vers les connexions de l’appelant : {id, connection_id, connection_name, folder, format, status, attempts, next_attempt, error_code, filename, published_at, created_at}. |
POST /api/projects/{id}/webdav-publications | {connection_id, folder, format} → 202 : met le volume terminé en file d’attente pour le worker. format vaut epub, epub-bilingual, docx (sans révisions), txt, txt-zip ou md. Quiconque peut lire le volume peut l’envoyer vers ses propres connexions. |
Un envoi passe de pending à published ou failed, avec les nouvelles tentatives de la publication en
bibliothèque (30 s, doublées jusqu’à une heure, huit tentatives). error_code indique pourquoi il attend
ou a échoué : incomplete, unreachable, server_error, build_failed donnent lieu à de nouvelles
tentatives ; unauthorized, secret_unreadable, refused, not_found, name_taken, too_large et
access_lost échouent immédiatement. Un dossier de destination absent est créé avec MKCOL avant de conclure
à not_found. Les erreurs de ces routes ne sont jamais 401 ni 403 — ceux-ci
appartiennent à la session Libris : un mot de passe refusé par le serveur WebDAV donne 422, un serveur
injoignable 502, une réponse plus grande que permis 413.
Erreurs
Toutes les erreurs ont la même forme :
{"detail": {"code": "chapter_conflict", "message": "…", "conflicts": ["…"]}}
code est stable ; message est en français par défaut et en anglais avec Accept-Language: en. Les
erreurs de validation ne renvoient jamais les valeurs soumises : le texte du livre n’est donc jamais
renvoyé.
| HTTP | code | Quand |
|---|---|---|
| 401 | missing_token, invalid_token, revoked_token, expired_token, inactive_account | Aucun jeton, ou un jeton invalide (en-tête WWW-Authenticate: Bearer). |
| 401 | unauthorized | Un corps de plus de 1 Mio envoyé sans en-tête Authorization: Bearer, refusé avant d’être lu. Une requête plus petite sans jeton reçoit missing_token. |
| 402 | automation_not_licensed | La licence de l’installation ne comprend pas l’API d’automatisation (formules Essai et Personnel ; Studio et Pro la comprennent). Les jetons existants sont conservés et refonctionnent dès que la licence la comprend. Répondu après la vérification du jeton : un jeton absent ou faux reçoit toujours son 401. |
| 402 | budget_exceeded (avec budget) | Le budget de coût du jeton est atteint : une requête qui lancerait du travail est refusée (voir Budget du jeton). |
| 403 | insufficient_scope (avec scope) | Il manque une permission au jeton. |
| 403 | forbidden | Une requête de navigateur venant d’un autre site (voir ci-dessous). |
| 403 | priority_not_allowed (avec max_priority) | La priorité demandée dépasse le plafond du jeton ou du compte. |
| 404 | request_not_found, series_not_found, volume_not_found, glossary_not_found, not_found | Inconnu, ou appartenant à quelqu’un d’autre. |
| 409 | idempotency_conflict (avec request_id) | Même clé ou même external_id, contenu différent. |
| 409 | chapter_conflict (avec conflicts) | Des chapitres existent avec un autre texte ; envoyez replace_changed_chapters. |
| 409 | conflict (avec protected_segments) | Un remplacement ferait perdre des modifications humaines ; envoyez discard_human. |
| 409 | volume_conflict, series_archived, volume_archived | Le volume cible ne peut pas recevoir ce contenu. |
| 409 | result_not_ready, request_failed, request_cancelled, format_unavailable | Le résultat ne peut pas être servi (voir Obtenir le résultat). |
| 409 | not_started, conflict | Pause, reprise ou annulation impossible dans l’état actuel (voir Mettre en pause, reprendre ou annuler). |
| 409 | glossary_exists, language_mismatch | Un glossaire partagé de ce nom existe ; ses langues diffèrent de celles de la série. |
| 409 | budget_exceeded | Reprise d’un travail mis en pause par un budget de livre ou de jeton encore atteint. |
| 413 | payload_too_large, glossary_too_large | Le corps dépasse la limite de taille. |
| 415 | unsupported_media_type | Ni JSON, ni EPUB, ni multipart. |
| 422 | invalid_payload (avec errors: [{loc, msg, type}]) | Le document ou l’envoi de fichier est invalide. |
| 422 | invalid_request (avec errors) | Un paramètre de requête incorrect (par exemple format, wait). |
| 422 | invalid_idempotency_key, unknown_provider, provider_required, invalid_epub, fixed_layout_epub, callback_refused, delivery_failed | Voir les sections précédentes. |
| 422 | invalid_placement | Le after d’un chapitre ne désigne aucun chapitre du volume ni de la requête. |
| 422 | invalid_glossary, invalid_strategy, invalid_mapping, invalid_name | Le fichier de glossaire, ses options d’import ou le nom du glossaire (voir Glossaires partagés). |
| 429 | rate_limited | Trop d’appels pour ce jeton (en-tête Retry-After). |
| 429 | queue_full (avec scope, limit) | Le jeton ou le compte a déjà son quota de requêtes en attente, lors d’une nouvelle requête ou d’une reprise (voir Priorité et quotas de la file d’attente). Pas de Retry-After : réessayez dès qu’un travail en attente a démarré. |
| 500 | server_error | Échec inattendu ; le message contient une référence de diagnostic pour les journaux du serveur. |
Les clients de serveur à serveur n’envoient pas d’en-tête Origin et sont acceptés. Une page de
navigateur d’un autre site est refusée comme pour l’interface (ALLOWED_ORIGINS, Sec-Fetch-Site).
Limites
| Réglage | Par défaut | Effet |
|---|---|---|
API_MAX_PAYLOAD_MB | MAX_UPLOAD_MB (256) | Plus grand corps de requête avec un jeton Bearer : un EPUB, ou l’ensemble des fichiers TXT. Appliqué avant la lecture du corps. Sans en-tête Bearer, la limite est de 1 Mio et la réponse 401. |
API_MAX_CHAPTERS | 2000 | Chapitres (ou fichiers TXT) par requête. |
TEXT_CHAPTER_MAX_CHARS | 2 000 000 | Caractères par chapitre, limite partagée avec les imports TXT. |
API_RATE_LIMIT_PER_MINUTE | 120 | Appels par jeton sur une minute glissante, comptés dans chaque processus d’API (avec plusieurs répliques de l’API, la limite effective est multipliée). 0 la désactive. |
API_RESULT_MAX_WAIT_SECONDS | 60 | ?wait= le plus long (0–600). |
API_REQUEST_STALL_MINUTES | 360 | Une requête dont le travail reste en pause, bloqué ou en attente plus longtemps échoue, et le travail est annulé (l’attente de l’analyse d’un volume précédent est bornée séparément). |
API_REQUEST_MAX_HOURS | 168 | Une requête encore inachevée au bout de ce délai échoue, et le travail est annulé. |
DELIVERY_REPAIR_ATTEMPTS | 3 | Tours de réparation EPUBCheck d’un EPUB livré. |
RETENTION_RESULTS_DAYS | 30 | Nombre de jours de conservation d’un fichier de résultat stocké (il peut être produit à nouveau ensuite). |
API_WEBHOOK_* | voir Webhooks | Hôtes, réseaux, secret, tentatives et délai d’expiration des webhooks. |
QUEUE_* | voir la configuration | Travaux par compte en cours et en attente, vieillissement des priorités ; un jeton peut avoir ses propres limites, plus basses. |
Un EPUB est aussi borné par les limites d’archive de tout import (MAX_UNPACKED_MB, MAX_ENTRIES,
MAX_COMPRESSION_RATIO). Tous les réglages sont décrits dans la référence de configuration.
Un exemple complet
Envoyer un EPUB, attendre la fin, puis télécharger le livre traduit et lire le rapport :
#!/usr/bin/env bash
set -euo pipefail
REQUEST_ID=$(curl -sS -X POST "$LIBRIS_URL/api/v1/translation-requests" \
-H "Authorization: Bearer $LIBRIS_TOKEN" \
-H "Idempotency-Key: book-fr-1" \
-F "file=@book.epub" -F target_language=fr -F provider_id="$PROVIDER_ID" \
| jq -r .request_id)
while :; do # chaque appel répond à la fin de la requête, ou au bout de 60 secondes
STATUS=$(curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID?wait=60" \
-H "Authorization: Bearer $LIBRIS_TOKEN" | jq -r .status)
case "$STATUS" in completed|completed_with_residuals|failed|cancelled) break ;; esac
done
echo "Request ended: $STATUS"
if [ "$STATUS" = completed ] || [ "$STATUS" = completed_with_residuals ]; then
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID/result" \
-H "Authorization: Bearer $LIBRIS_TOKEN" -o book.fr.epub
fi
curl -sS "$LIBRIS_URL/api/v1/translation-requests/$REQUEST_ID" \
-H "Authorization: Bearer $LIBRIS_TOKEN" | jq '.report | {outcome, residual_total, usage}'
Liens de relecture publics
Les liens de relecture sont distincts de /api/v1 : ils autorisent la lecture d’un volume sans compte,
pas l’automatisation. L’adresse partagée reste /#review/<secret> ; le fragment n’est pas envoyé au
serveur. Le lecteur envoie Authorization: Bearer <secret> à ces deux endpoints :
| Endpoint | Page bornée | Suite |
|---|---|---|
GET /api/review/book?offset=0 | 100 titres de chapitres, plus le titre, l’auteur, la langue cible, l’étiquette du lien et la visibilité du texte source | next_offset, ou null à la fin |
GET /api/review/chapters/{chapter_id}?offset=0 | 2 000 passages traduits, tels que les donne un export ; le texte original seulement si le lien l’autorise explicitement | next_offset, ou null à la fin |
Les décalages (offsets) doivent être des entiers compris entre 0 et 2 147 483 647. Un identifiant absent,
expiré, révoqué ou inconnu renvoie le même 404 ; le chapitre d’un autre volume ne peut pas être lu. Les
cookies de session ne remplacent jamais le lien. Un vrai lien sur une installation dont la licence ne comprend
pas le partage (seules Studio et Pro le comprennent) répond 402 avec code: "sharing_not_licensed" : le lien
est suspendu, pas supprimé, et s’ouvre de nouveau dès que la licence accorde le partage. Créer un lien ou
inviter un membre est refusé de la même façon. Toutes les réponses de l’API, refus compris, utilisent
Cache-Control: no-store. La révocation est vérifiée à chaque page ; un texte déjà lu ou copié ne peut
pas être repris.
Les lectures sont limitées, avant toute requête SQL, à 120 par adresse IP cliente et à 600 au total sur
une minute glissante, par processus d’API. Une réponse 429 contient Retry-After ; les nouvelles
tentatives avec des secrets différents partagent les mêmes limites. Un proxy inverse doit transmettre
l’adresse réelle du client via la configuration des proxys de confiance de l’installation. Le compteur de
visites regroupe les ouvertures de la table des matières en au plus un incrément atomique par lien et par
minute ; c’est un indicateur d’activité, pas un décompte exact des visiteurs.
Le lecteur de la version 0.10.0 plaçait le secret dans les chemins des requêtes d’API. Ces anciens chemins ne sont plus servis, tandis que les liens déjà émis s’ouvrent toujours dans le navigateur avec le lecteur mis à jour. Si des journaux d’accès historiques ont été partagés ou exposés, révoquez les liens concernés et créez-en de nouveaux : cette mise à jour ne peut pas effacer les anciens journaux.
Lecteur intégré (API de l’interface)
L’interface connectée utilise GET /api/projects/{pid}/read/{chapter_id}, pas /api/v1. Une session et
l’accès au volume sont exigés ; les jetons d’API et les liens de relecture publics ne donnent pas accès.
Un chapitre appartenant à un autre volume est refusé avec 404.
offset commence à zéro et est borné à 2 147 483 647 ; limit vaut 100 par défaut et accepte 1 à 250.
La réponse contient chapter, can_edit, passages et next_offset (null à la fin). Chaque passage
contient id, revision, translation, retained_source et les units ordonnées, avec leurs
identifiants, le texte traduit et les marqueurs EPUB protégés. translation est le texte que donne un
export — un passage de la machine dans la typographie de la langue cible, même traduit avant la 0.18 —,
alors que les units portent le texte enregistré dont part une correction, comme dans l’éditeur.
source=true renvoie en plus le texte source ; les métadonnées d’import et l’analyse ne sont jamais
incluses. Un original explicitement conservé reste le texte affiché et est signalé par retained_source,
sans être présenté comme une traduction.
Le lecteur n’ajoute aucun endpoint d’écriture. Les corrections passent par l’endpoint existant
PUT /api/segments/{sid} avec revision et le tableau complet et ordonné des units (id, text). Cet
endpoint refuse les collaborateurs en lecture seule. Une révision périmée renvoie 409 au lieu d’écraser
une correction concurrente. Les corrections réussies entrent dans le même historique des versions humaines
que les modifications faites dans l’éditeur côte à côte.
Lancer une série entière
POST /api/v1/series/{series_id}/jobs/start lance le pipeline complet sur chaque volume d’une série en
une seule requête. Il exige jobs:control et pipeline:start : le contrôle permet d’arrêter une
série, seule la seconde permission peut en facturer une. Le navigateur utilise
POST /api/series/{id}/jobs/start.
Le rapport a la même forme « au mieux » que les autres actions de série — une ligne par volume avec
done, jobs, et un code/message en cas de refus. Un volume déjà en cours répond already_running
et n’est pas touché ; les volumes archivés ne sont pas inclus du tout ; une série suspendue refuse l’appel
entier avec series_suspended plutôt que de reprendre d’elle-même en silence — exactement comme le bouton
de la page de la série, qui n’affiche jamais d’action de lancement pendant la pause, seulement Reprendre
la série. Un appelant qui ajoute ?resume_if_paused=true lève la pause et lance dans la même requête ;
désactivé par défaut, et jamais utilisé par l’interface — ne le demandez que lorsque lever la pause dans le
même appel est l’intention réelle et délibérée (une automatisation, pas le clic d’une personne).
Suspension persistante d’une série
POST /api/v1/series/{series_id}/jobs/{action} exige jobs:control. Le compte du jeton doit être
propriétaire de la série ; l’accès à un volume partagé ne suffit pas. action vaut pause, resume ou
cancel. Aucun corps de requête n’est nécessaire.
La pause enregistre paused_at sur la série et met en pause ses travaux retenus en une seule requête
serveur. Elle empêche tout nouveau travail payant, y compris les démarrages automatiques de la
surveillance des sources. Les chapitres importés restent disponibles ; la lecture, les corrections
manuelles et les exports ne sont pas affectés. Les appels au provider déjà envoyés peuvent encore
aboutir. La reprise efface d’abord paused_at, puis applique les contrôles habituels des travaux à chaque
volume. L’annulation ne modifie pas paused_at et n’empêche pas à elle seule les futurs lancements de la
surveillance des sources.
La réponse est HTTP 200 pour une action de série valide, même quand certains volumes refusent :
{
"series_id": "...", "action": "resume", "paused_at": null,
"done": 2, "refused": 1, "deferred": 1,
"volumes": [
{"project_id": "one", "title": "Volume 1", "jobs": 1, "done": true},
{"project_id": "two", "title": "Volume 2", "jobs": 0, "done": false,
"code": "budget_exceeded", "message": "Budget du livre atteint (...)"},
{"project_id": "three", "title": "Volume 3", "jobs": 1, "done": true,
"deferred": true, "message": "Reprise planifiée : le travail attend une place dans la file équitable."}
]
}
done compte les volumes pour lesquels la commande a réussi (y compris les volumes sans travail
éligible), pas les travaux en cours. jobs compte les changements validés pour ce volume. deferred est
un sous-ensemble de done : la file d’attente était pleine, la reprise du volume est donc enregistrée et
sera prise en charge automatiquement dans le respect des quotas d’exécution habituels. La vue de la file
d’attente inclut ces reprises. Aucun jeton ne peut reprendre un travail payant lancé par un autre jeton
ou par une session de navigateur. Les vérifications de licence, de nombre de comptes autorisés et de
budget sont refaites à la reprise, quelle que soit la raison de l’arrêt précédent. Les codes HTTP
401/403/404 rejettent toujours l’authentification, la portée ou la propriété de la série avant tout
changement. Les messages de résultat suivent Accept-Language.
Le navigateur utilise POST /api/series/{id}/jobs/{action} pour une suspension durable de série. Sa
route distincte POST /api/projects/batch/jobs/{action} accepte un tableau project_ids (1 à 500) et un
filtre d’opérations facultatif ; elle ne contrôle que les travaux sélectionnés, jamais la suspension
persistante d’une série entière.
Archives de série (API de session)
Une série entière passe d’une installation à l’autre sous la forme d’un seul fichier. Ce sont des routes
de session (celles de l’interface), pas /api/v1 : l’archive de projet d’un volume isolé ne fait pas
non plus partie de l’API d’automatisation ni du MCP.
GET /api/series/{id}/export— réservé au propriétaire de la série (404sinon). Répondapplication/zip:series.jsonet une archive de projet par volume (volumes/<n>.zip). Refusé avec413quand l’import ne pourrait pas la relire (MAX_UPLOAD_MB,MAX_ENTRIES, ou un volume qui dépasse ses propres limites, nommé dans le message), et avec409quand les fichiers source d’un volume manquent sur le serveur.POST /api/series/import— champ multipartfile. Crée une nouvelle série de l’appelant et répond201avecid,name,renamed_from(le nom de l’archive quand il était déjà pris et que la série est devenueName (2)…, sinonnull) etvolumes(les identifiants des nouveaux projets, dans l’ordre de l’archive). Tout est vérifié avant la moindre écriture :422nomme le champ incorrect (links.0.entity_id,entities.3.merged_into_id…) ou le volume et son problème ;409quand l’EPUB d’un volume est déjà dans la bibliothèque de l’appelant ;413au-delà deMAX_UPLOAD_MB. Une archive de volume envoyée ici, ou une archive de série envoyée àPOST /api/projects/import, est refusée avec un message qui indique la bonne route.
Les providers, les membres, la suspension de la série, le journal d’audit, le glossaire partagé associé, les requêtes d’automatisation et les documents OpenViking ne sont pas transportés ; les travaux interrompus reviennent en pause. Format et vérifications : architecture.md (en anglais).
Lots des providers et estimations
Les routes de session de création et de modification des providers acceptent un objet
capabilities.batch :
{"enabled": true, "kind": "openai", "max_wait_hours": 24}
kind vaut openai ou anthropic et doit correspondre au transport du provider. Les connexions de
compte ChatGPT ne peuvent pas activer les lots. enabled est un booléen strict ; max_wait_hours est un
entier de 1 à 24. C’est le délai local au bout duquel Libris demande l’annulation, pas une garantie de
niveau de service du provider. La fenêtre d’exécution soumise à OpenAI reste 24h. Les étapes
dépendantes peuvent nécessiter plusieurs lots.
Saisissez les prix synchrones par million de jetons, pas les prix remisés. Les prévisions des travaux (y
compris estimate_job) et les estimations avant import utilisent la moitié de ces tarifs quand le mode
par lots est activé. Leur objet batch expose enabled et, s’il est activé, max_wait_hours ;
currency_note explique l’hypothèse et le repli synchrone au tarif normal. Les nombres de jetons sont
inchangés. Les journaux des requêtes réelles enregistrent les tarifs en vigueur à la soumission, et non
ceux configurés au moment où les résultats arrivent.
GET /api/projects/{pid}/batch est une route de session en lecture seule, avec les droits de lecture
habituels sur le livre. Elle renvoie null quand le dernier travail n’a pas de résumé de lot, sinon :
{"job_id": "...", "status": "waiting", "batch": {
"state": "submitted", "pending": 412, "collected": 500,
"submitted_at": 1790000000, "next_poll_at": 1790000300
}}
Tous les champs du lot sauf state sont facultatifs pendant les transitions. Les états sont collecting,
uploading, submitting, submitted, canceling, ready, unknown, unsupported. Le résumé peut aussi
contenir provider_id, submission_key, remote_id, last_polled_at, poll_attempts et un message
sans données sensibles. Il ne renvoie jamais le point de reprise brut, les identifiants d’API, les prompts
collectés ni le corps des résultats. Les nombres sont des requêtes, pas des passages uniques. Les codes
HTTP 401/404 protègent l’authentification et l’isolement entre comptes.
unknown signifie que l’acceptation doit être vérifiée, et non qu’une nouvelle soumission automatique est
sans risque. unsupported annonce le repli synchrone au plein tarif du travail en cours, faute d’API Batch
disponible. L’annulation est une demande adressée au provider ; les appels déjà terminés peuvent rester
facturables. La récupération de résultats déjà payés continue en dehors de la plage de travail configurée ;
les nouvelles soumissions restent soumises aux garde-fous de dépense.
POST /api/projects/{pid}/jobs/{jid}/batch/reconcile est une route de session réservée au propriétaire
du livre. Elle accepte {"remote_id":"...","submission_key":"...","confirm":true} ; la confirmation est
un booléen littéral. Elle rattache une soumission unknown à un lot trouvé dans le tableau de bord du
provider, et ne crée jamais d’autre lot. OpenAI doit renvoyer les métadonnées de soumission uniques et
l’identifiant du fichier envoyé.
Anthropic doit avoir terminé et exposer exactement les identifiants de résultats attendus. Un lot étranger
ou un lot Anthropic inachevé renvoie 409 ; les échecs de vérification auprès du provider renvoient 502.
L’état de pause ou d’annulation humaine du travail est conservé pendant que la réconciliation en arrière-plan
reprend. Ne supprimez jamais les lignes de requêtes en attente et ne retirez jamais les identifiants du
provider pour contourner cette vérification : elles conservent la facture en cours.
La collecte est limitée à 500 requêtes et 20 Mio par vague ; les flux de résultats sont limités à 64 Mio. Les échecs transitoires connus d’un élément ont au plus trois tentatives distantes ; les éléments voisins réussis ne sont pas soumis à nouveau. Un POST non acquitté et un lot terminé auquel il manque un résultat ne déclenchent jamais de nouvelle tentative payante à l’aveugle. La rétention des requêtes préserve les engagements en attente et les résultats nécessaires à l’état de leur travail.
Administration SMTP (API de session)
Ces routes de l’installation exigent une session d’administrateur, pas un jeton d’automatisation. Elles
sont volontairement en dehors de /api/v1 et de son contrat OpenAPI.
| Route | Rôle |
|---|---|
GET /api/settings/mail | Réglages effectifs, saved, configured, has_password ; jamais de mot de passe |
PUT /api/settings/mail | Remplacement complet : enabled, host, port, starttls, username, sender, timeout, et password facultatif, en écriture seule |
DELETE /api/settings/mail | Rétablit les valeurs d’environnement de l’installation |
POST /api/settings/mail/test | { "recipient": "reader@example.com" } ; 202 avec queued et l’id du message |
GET /api/settings/mail/outbox | `status=pending |
POST /api/settings/mail/outbox/{id}/retry | Remet en file un message en échec, sauf les liens de récupération de mot de passe |
Un mot de passe omis ou null est conservé ; une chaîne vide explicite l’efface. Les lignes de la boîte
d’envoi n’exposent que le destinataire, l’objet, le type, l’état et les heures d’envoi, les tentatives et
un code ou message d’erreur sans données sensibles. Les corps, les contextes et les réponses SMTP brutes
sont exclus. Les actions de configuration, de test et de nouvelle tentative sont journalisées dans l’audit,
sans identifiants. Le trafic SMTP a lieu dans le worker, jamais pendant la requête HTTP de test.
sending signifie qu’un worker détient un bail d’envoi renouvelable ; son jeton interne de propriété n’est
jamais renvoyé. next_attempt désigne alors l’expiration du bail, pas un second envoi programmé. La
nouvelle tentative reste réservée aux messages en échec et remet à zéro leur quota de tentatives sans
réutiliser un ancien bail.
Mention de traduction assistée par IA (API de session)
Tout EPUB écrit par Libris porte une mention lisible par machine « traduction assistée par IA » (#284). Elle est activée par défaut et suit deux réglages, celui du livre en premier :
- Installation (administrateur) :
GET/PUT /api/settings/exports, corps et réponse{"ai_disclosure": bool}(absent =true). Stocké dansapp_settings(cléexports), sans changement de schéma. - Livre (qui peut modifier le volume) :
PATCH /api/projects/{pid}/editionavec{"ai_disclosure": true | false | null};nullrevient au choix de l’installation. GETetPATCH /api/projects/{pid}/editionrenvoientai_disclosure(la valeur du prochain export : choix du livre, sinon celui de l’installation),ai_disclosure_default(celui de l’installation) etai_disclosure_book(choix propre au livre,null= suit l’installation). Une interface afficheai_disclosureet ne l’écrit que lorsque l’utilisateur bascule l’interrupteur : un livre jamais touché continue de suivre l’installation.
La valeur effective s’applique à tous les chemins EPUB : GET /api/projects/{pid}/export/epub et
epub-bilingual (plages de chapitres comprises), POST /api/exports/epub, requêtes de l’API v1, publication
en bibliothèque, WebDAV et envoi par courriel. Un changement touche le prochain EPUB écrit, pas les fichiers déjà
livrés.
Publication en bibliothèque (API de session)
Ces routes exigent une session ; les jetons d’automatisation n’y donnent pas accès. Un administrateur de
l’installation utilise GET, PUT ou DELETE /api/settings/library. PUT accepte enabled, directory,
automatic et series_folders ; DELETE rétablit DELIVERY_DIR. Le répertoire doit déjà être monté dans
le worker. La publication utilise le système de fichiers de ce worker, pas une requête HTTP vers une
bibliothèque.
POST /api/projects/{pid}/publish est réservé au propriétaire et renvoie 202 avec l’état durable de la
publication. Les requêtes répétées pendant l’attente sont regroupées. GET /api/projects/{pid}/publication
est accessible aux utilisateurs qui peuvent lire le volume et renvoie enabled, id, status (none,
pending, published, failed), attempts, next_attempt, error_code, filename et published_at.
Aucun chemin du serveur ni aucune exception brute n’est renvoyé. Une publication en échec peut être remise
en file par POST. Huit tentatives sont permises ; les redémarrages du worker récupèrent les baux de cinq
minutes expirés. Ces routes ne lancent ni ne facturent aucun modèle.
Changer la destination configurée refuse une destination plus ancienne encore en attente avec
configuration_changed ; une nouvelle tentative explicite choisit le nouveau dossier. published prouve
la remise atomique du fichier, pas son indexation en aval. Les lecteurs doivent configurer une analyse
Komga/Kavita ou un import Calibre séparé. Voir le guide de publication.
Les EPUB publiés portent le nom actuel de la série Libris et, le cas échéant, le numéro du volume, au moyen
des métadonnées EPUB 3 belongs-to-collection, collection-type=series et group-position (pour un
volume spécial dont l’étiquette suit un numéro, comme 14+, la position est 14.5 ; une étiquette placée
en fin de série n’écrit aucune position). Le titre du volume et les chemins stables du système de
fichiers, fondés sur des UUID, restent distincts. Les sources EPUB 2 sont mises à niveau au moyen de la
conversion existante, qui préserve les ressources, et du contrôle de validation. Des métadonnées modifiées
exigent une nouvelle publication et une nouvelle analyse par le lecteur ; les identifiants de regroupement
internes et la progression de lecture d’un lecteur échappent au contrôle de Libris.
Premiers pas (API de session)
La liste du premier lancement se lit dans ce que l’installation contient déjà ; le serveur de licences
n’est pas interrogé. Les deux routes exigent une session d’administrateur ; un membre reçoit 403.
| Méthode et chemin | Corps et résultat |
|---|---|
GET /api/onboarding | {steps, providers_priced, completed, dismissed, visible}. steps liste, dans l’ordre, {key, done} pour licence (un certificat signé est détenu), provider (un provider non retiré), provider_tested (un POST /api/providers/{id}/test réussi, ou un appel au modèle déjà fait), first_book (un livre existe) et admin_password (le compte BOOTSTRAP_USERNAME n’a plus BOOTSTRAP_PASSWORD). providers_priced vaut false quand aucun provider n’a de prix d’entrée ni de sortie : les estimations d’import afficheront alors 0. |
PUT /api/onboarding | {dismissed: true} masque la liste, {dismissed: false} la réaffiche. Renvoie la même vue. |
visible vaut true tant qu’une étape reste à faire et que la liste n’est pas masquée. Ce n’est
qu’une indication pour l’interface : aucune étape ne bloque quoi que ce soit.