Source docs/api.fr.md · 1de96aa

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) ou cancelled. Elle ne reste jamais indéfiniment running.

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 cheminPortéeRôle
POST /api/v1/translation-requestscontent: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}/pausejobs:controlMettre en pause le travail de la demande
POST /api/v1/translation-requests/{id}/resumejobs:controlLe reprendre
POST /api/v1/translation-requests/{id}/canceljobs:controlL’annuler
GET /api/v1/translation-requests/{id}/resultresults:readTé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:readLire 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/providerscontent:writeLister les providers qu’une demande peut utiliser
GET /api/v1/seriesseries:readLister vos séries
GET /api/v1/series/{id}series:readUne série et ses volumes
POST /api/v1/series/{id}/jobs/{action}jobs:controlSuspendre, reprendre ou annuler une série, avec un rapport par volume ; voir Suspension persistante d’une série
GET /api/v1/glossariesseries:readLister vos glossaires partagés
POST /api/v1/glossariescontent:writeCréer un glossaire partagé
GET /api/v1/glossaries/{id}series:readUn glossaire partagé et ses termes
GET /api/v1/glossaries/{id}/export/{format}series:readLe télécharger en JSON, CSV ou TBX
POST /api/v1/glossaries/{id}/importcontent:writeImporter un fichier JSON, CSV ou TBX (?dry_run=true pour un aperçu)
GET /api/v1/series/{id}/shared-glossaryseries:readLe glossaire partagé que suit une série
PUT /api/v1/series/{id}/shared-glossarycontent:writeRattacher une série à un glossaire partagé, ou l’en détacher
GET /api/v1/booksnarrative:readLister vos livres
GET /api/v1/books/{id}/narrative-contextnarrative:readLe Narrative Context Bundle d’un livre : son texte et sa mémoire (voir Contexte narratif)
GET /api/v1/books/{id}/narrative-context/revisionnarrative:readLes empreintes du bundle seules
GET /api/v1/series/{id}/narrative-contextnarrative:readLa 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.json décrit /api/v1 en 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.py est 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éponses 429 rate_limited (il signale 429 queue_full au 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 8080
    

    Sa classe LibrisClient peut aussi être importée dans votre propre code ; lancez-le avec --help pour voir toutes les options. Le client n’envoie le jeton qu’à l’origine de LIBRIS_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 de https à http — avec le code d’erreur redirect_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

  1. Dans l’interface, ouvrez Mon compte › Jetons d’API (les administrateurs le trouvent aussi sous Paramètres › API d’automatisation).
  2. 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).
  3. 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).
  4. 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éeLibellé dans l’interfaceAutorise
series:readLire les sériesGET /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:writeEnvoyer du contenuPOST /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:startLancer le pipelineAvec content:write : les demandes qui lancent la traduction (le comportement par défaut). Sans elle, seul start=false (import seul) est accepté.
jobs:readSuivre les travauxGET /api/v1/translation-requests/{id}
jobs:controlPiloter les travaux (pause, reprise, annulation)POST …/pause, …/resume, …/cancel
results:readLire les résultatsGET /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 :

RouteCorps et réponse
GET /api/tokensLes 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/tokensCorps {"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}/queueCorps {"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}/budgetCorps {"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_priority du jeton (normal sauf réglage contraire) ni le plafond du compte (high pour les administrateurs et pour les comptes qu’un administrateur a autorisés dans Paramètres › File d’attente, normal sinon ; un administrateur peut aussi y abaisser un compte à low). Au-delà, la demande est refusée avec 403 priority_not_allowed et max_priority dans l’erreur. Sans priorité, une demande s’exécute en normal, ou au plafond inférieur d’un jeton ou d’un compte limité à low.
  • max_running limite les travaux du jeton exécutés simultanément : les suivants attendent (queue.reason vaut token_limit). La limite propre au compte (QUEUE_MAX_RUNNING_PER_ACCOUNT ou sa ligne dans Paramètres › File d’attente) s’applique aussi (account_limit).
  • max_queued limite 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 avec 429 queue_full et scope (token ou account) et limit dans 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 de Retry-After. Le rejeu d’une demande acceptée (même Idempotency-Key ou même external_id) reçoit toujours une réponse. Reprendre une demande en pause compte comme une nouvelle entrée dans la file, donc …/resume peut lui aussi répondre 429 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 (start vrai, par défaut) est refusée avec 402 budget_exceeded ; l’erreur porte budget: {amount, spent, period, resets_at} (resets_at : début du mois suivant, null pour 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. …/resume répond 409 budget_exceeded tant que le plafond n’a pas été relevé (PUT /api/tokens/{id}/budget). Une demande laissée en pause plus longtemps que API_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 avec 202 puis refusée à ce stade se termine en failed, avec la raison dans error.
  • 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 avec 402 licence_quota_insufficient — ou 402 allowance_insufficient pour le quota mensuel propre au compte — avec words et left (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 failed avec stop_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, comme POST /api/projects/{id}/jobs dans le navigateur, répond 402 licence_quota_insufficient pour la traduction d’un livre entier, avec words (ce que le livre coûterait) et left.
  • 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éeComment l’envoyerRésultat par défaut
Un EPUBmultipart/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êteL’EPUB traduit
Des chapitres TXT ou DOCXmultipart/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 chapitreJSON
Un document JSONContent-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 par series_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épond 409 volume_conflict.
  • Les langues sont par défaut la langue déclarée dans l’EPUB (en s’il n’y en a pas) et la langue cible de la série (fr s’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épond 422 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 dans report.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.txt avec 12.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 dans report.decisions.intake avec une confiance faible.
  • Les titres sont tirés des noms de fichier.
  • Les fichiers DOCX s’envoient de la même façon (.docx au 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 comme Chapter 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 ; un One Thousand non 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 (kind front_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) et Chapter 12 (2/2) sont les deux parties du chapitre 12. Un prologue, un interlude, une histoire annexe ou un épilogue garde son kind et 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.items du 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=headings prend exactement un fichier (422 invalid_payload sinon) 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"
ChampObligatoireRègles
external_idnonVotre 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).
seriesouiid (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.numberoui, sauf avec volume.latest1–10000.
volume.latestnonfalse 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.titlenonLe 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.
authornonJusqu’à 500 caractères.
source_language, target_languageouiBalises BCP 47 comme en, fr-FR, zh-Hant, es-419. Appliquées au volume.
chaptersouiDe 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_chaptersnonfalse 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_humannonfalse 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.startnontrue par défaut : exécuter tout le pipeline (exige la portée pipeline:start). false se contente d’importer les chapitres.
pipeline.provider_idnonVoir Choisir un provider.
pipeline.qualitynonfast, normal, high ou maximum (voir le guide du pilote automatique).
pipeline.context_backendnoninternal, openviking ou hybrid (voir OpenViking).
pipeline.final_reviewnontrue par défaut. false saute la relecture finale. Elle ne s’exécute jamais quand le serveur définit FINAL_REVIEW_ENABLED=false.
pipeline.prioritynonlow, normal (par défaut) ou high, dans la limite du plafond du jeton (voir Priorité et quotas de la file d’attente).
pipeline.analysis_modenonparallel ou strict ; par défaut : le choix du volume, sinon ANALYSIS_MODE (parallel). Voir Modes d’analyse.
pipeline.threadsnon1–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_idnonLe 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.formatnonFormat par défaut du résultat : json, txt, txt-zip ou epub-bilingual.
callback_urlnonUn webhook appelé quand la demande se termine (voir Webhooks).
callback_eventsnonÉ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": "…"}
]
ChampSignification
kindchapter (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.
numberPour 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_countPartie 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.
labelLe nom affiché après le type (Ayla dans Interlude – Ayla), 200 caractères au plus.
afterInsé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.
positionInsé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 alors waiting avec stop_reason: earlier_volume, et queue.reason vaut earlier_volume). Cette attente dure au plus API_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.

OptionSignification
series ou series_idLa 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).
volumeNumé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_idVotre identifiant de la demande.
volume_external_idVotre identifiant du volume.
title, authorTitre et auteur du volume (sinon, un EPUB garde les siens).
source_language, target_languageBalises BCP 47. Toutes deux obligatoires pour du TXT.
provider_id, quality, context_backend, final_review, priority, analysis_mode, threadsComme dans pipeline plus haut.
starttrue (par défaut) exécute tout le pipeline ; false se contente d’importer.
output_formatepub (entrée EPUB seulement ; la valeur par défaut pour un EPUB), json, txt, txt-zip ou epub-bilingual.
callback_urlVoir 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_humanTXT et DOCX seulement, comme dans le document JSON.
splitTXT 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.
filenameCorps 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ù after les place (voir Chapitres irréguliers), et mis en correspondance par external_id, puis par type, numéro et partie : un chapitre renvoyé avec le même texte est unchanged, un chapitre avec un autre texte est refusé, sauf si replace_changed_chapters est 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 scope dans 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

  1. 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é.
  2. 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.)
  3. Finalisation. Le travail est terminé ; Libris construit et stocke le résultat, puis rédige le rapport.
  4. 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.

ÉtatSignification
queuedEn attente que le volume soit libre.
importedChapitres importés, rien n’a démarré (start valait false). C’est un état final.
pending, runningLe travail attend un worker, ou est en cours.
pausedMis en pause par vous ou par une personne dans l’interface.
waitingLe provider est temporairement indisponible, nouvelle tentative automatique ; ou, avec stop_reason: earlier_volume, l’analyse attend un volume antérieur de la série.
blockedDemande une intervention, par exemple quand le provider refuse ses identifiants.
finalizingLe travail est terminé ; le résultat est en cours de construction.
completedTous les passages sont traduits ; le résultat est stocké.
completed_with_residualsLe 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.
failedVoir 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.
cancelledAnnulé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
}
ChampSignification
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).
progresssegments, 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.
estimateTemps et coût restants, une fois observés suffisamment d’appels au modèle ; sinon null.
error, stop_reason, next_attemptPourquoi le travail s’est arrêté ou attend, et quand il fera une nouvelle tentative (heure Unix, 0 s’il n’attend pas).
priorityLa priorité de la demande (low, normal, high), telle que modifiée par une personne dans l’interface le cas échéant.
queueTant 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.
chaptersCombien 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.
resultUne fois stocké : format, media_type, filename, size, sha256, created_at.
reportLe rapport de fin, une fois la demande terminée.
webhookSeulement quand un callback_url a été fourni : state (pending, delivered, failed), attempts, dernière error.
chapter_eventsSeulement 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 :

scopeChapitres
request (par défaut)Les chapitres envoyés par la demande, y compris ceux qui sont unchanged ; pour un EPUB, le livre entier.
newSeulement les chapitres que la demande a créés ou remplacés : les nouveaux chapitres d’une suite.
volumeTous 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"}
}
  • sha256 est le SHA-256 de translation (UTF-8) ; source_sha256 est 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 son external_id. display_label est 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.
  • issues liste les problèmes de qualité non résolus (segment_id, severity, code, message) ; flagged_passages liste les passages toujours signalés (check, error ou refused, non validés).
  • strategy ne nomme que le provider et le modèle, jamais l’adresse ni la clé du provider.
  • report est le rapport de fin une fois la demande terminée, null avant.

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": "…", "…": "…"}]}
}
ChampSignification
outcome, reasoncompleted, completed_with_residuals, failed ou cancelled, et la raison quand la demande n’a pas abouti.
passagesDécomptes portant sur les passages de la demande (le livre entier pour un EPUB).
residualsPassages 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.
usageAppels 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.
costL’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.
durationsSecondes écoulées depuis la création de la demande, passées à attendre le volume, et passées dans le travail.
autopilotComment le pilote automatique s’est terminé (null quand il ne s’est pas exécuté).
decisionsautopilot : 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_mapChaque 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).
qualityScores 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.
deliveryEPUB 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églageVariable d’environnementValeur 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 CIDRAPI_WEBHOOK_PRIVATE_NETWORKSvide
Nombre maximal de tentativesAPI_WEBHOOK_MAX_ATTEMPTS6
Délai d’expiration d’un appel, en secondesAPI_WEBHOOK_TIMEOUT_SECONDS10
Secret de signature global (32 caractères au moins)API_WEBHOOK_SECRETvide

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êteValeur
X-Libris-Eventtranslation_request.finished
X-Libris-Delivery<identifiant de la requête>:<numéro de la tentative>
X-Libris-TimestampHeure Unix en secondes
X-Libris-Signaturesha256=<hex> : HMAC-SHA256 de <timestamp>.<body>
User-AgentLibris-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 http ou https sans 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 :

ChampValeursPar défaut
fileLe fichier du glossaire (2 Mo au plus)obligatoire
strategyskip conserve les termes en place ; replace remplace les termes non verrouillés qui diffèrent ; replace_all remplace aussi les termes verrouillésskip
delimitersemicolon, comma ou tabdétecté
mappingObjet 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
headertrue ou false : la première ligne contient-elle les noms de colonnesdétecté
skip_invalidtrue écarte les lignes invalides au lieu de refuser le fichierfalse

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 cheminCorps et résultat
GET /api/usersListe 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-factorRetire 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’annuaireContrat
GET /api/auth/ldapPublique. {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/ldapAdministrateur. 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/testAdministrateur. {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}/directoryAdministrateur, 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 sessionContrat
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/beginRenvoie {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/beginRenvoie {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 cheminCorps 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}/testPROPFIND (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-publicationsLes 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é.

HTTPcodeQuand
401missing_token, invalid_token, revoked_token, expired_token, inactive_accountAucun jeton, ou un jeton invalide (en-tête WWW-Authenticate: Bearer).
401unauthorizedUn 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.
402automation_not_licensedLa 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.
402budget_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).
403insufficient_scope (avec scope)Il manque une permission au jeton.
403forbiddenUne requête de navigateur venant d’un autre site (voir ci-dessous).
403priority_not_allowed (avec max_priority)La priorité demandée dépasse le plafond du jeton ou du compte.
404request_not_found, series_not_found, volume_not_found, glossary_not_found, not_foundInconnu, ou appartenant à quelqu’un d’autre.
409idempotency_conflict (avec request_id)Même clé ou même external_id, contenu différent.
409chapter_conflict (avec conflicts)Des chapitres existent avec un autre texte ; envoyez replace_changed_chapters.
409conflict (avec protected_segments)Un remplacement ferait perdre des modifications humaines ; envoyez discard_human.
409volume_conflict, series_archived, volume_archivedLe volume cible ne peut pas recevoir ce contenu.
409result_not_ready, request_failed, request_cancelled, format_unavailableLe résultat ne peut pas être servi (voir Obtenir le résultat).
409not_started, conflictPause, reprise ou annulation impossible dans l’état actuel (voir Mettre en pause, reprendre ou annuler).
409glossary_exists, language_mismatchUn glossaire partagé de ce nom existe ; ses langues diffèrent de celles de la série.
409budget_exceededReprise d’un travail mis en pause par un budget de livre ou de jeton encore atteint.
413payload_too_large, glossary_too_largeLe corps dépasse la limite de taille.
415unsupported_media_typeNi JSON, ni EPUB, ni multipart.
422invalid_payload (avec errors: [{loc, msg, type}])Le document ou l’envoi de fichier est invalide.
422invalid_request (avec errors)Un paramètre de requête incorrect (par exemple format, wait).
422invalid_idempotency_key, unknown_provider, provider_required, invalid_epub, fixed_layout_epub, callback_refused, delivery_failedVoir les sections précédentes.
422invalid_placementLe after d’un chapitre ne désigne aucun chapitre du volume ni de la requête.
422invalid_glossary, invalid_strategy, invalid_mapping, invalid_nameLe fichier de glossaire, ses options d’import ou le nom du glossaire (voir Glossaires partagés).
429rate_limitedTrop d’appels pour ce jeton (en-tête Retry-After).
429queue_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é.
500server_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églagePar défautEffet
API_MAX_PAYLOAD_MBMAX_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_CHAPTERS2000Chapitres (ou fichiers TXT) par requête.
TEXT_CHAPTER_MAX_CHARS2 000 000Caractères par chapitre, limite partagée avec les imports TXT.
API_RATE_LIMIT_PER_MINUTE120Appels 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_SECONDS60?wait= le plus long (0–600).
API_REQUEST_STALL_MINUTES360Une 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_HOURS168Une requête encore inachevée au bout de ce délai échoue, et le travail est annulé.
DELIVERY_REPAIR_ATTEMPTS3Tours de réparation EPUBCheck d’un EPUB livré.
RETENTION_RESULTS_DAYS30Nombre de jours de conservation d’un fichier de résultat stocké (il peut être produit à nouveau ensuite).
API_WEBHOOK_*voir WebhooksHôtes, réseaux, secret, tentatives et délai d’expiration des webhooks.
QUEUE_*voir la configurationTravaux 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 :

EndpointPage bornéeSuite
GET /api/review/book?offset=0100 titres de chapitres, plus le titre, l’auteur, la langue cible, l’étiquette du lien et la visibilité du texte sourcenext_offset, ou null à la fin
GET /api/review/chapters/{chapter_id}?offset=02 000 passages traduits, tels que les donne un export ; le texte original seulement si le lien l’autorise explicitementnext_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 (404 sinon). Répond application/zip : series.json et une archive de projet par volume (volumes/<n>.zip). Refusé avec 413 quand 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 avec 409 quand les fichiers source d’un volume manquent sur le serveur.
  • POST /api/series/import — champ multipart file. Crée une nouvelle série de l’appelant et répond 201 avec id, name, renamed_from (le nom de l’archive quand il était déjà pris et que la série est devenue Name (2)…, sinon null) et volumes (les identifiants des nouveaux projets, dans l’ordre de l’archive). Tout est vérifié avant la moindre écriture : 422 nomme le champ incorrect (links.0.entity_id, entities.3.merged_into_id…) ou le volume et son problème ; 409 quand l’EPUB d’un volume est déjà dans la bibliothèque de l’appelant ; 413 au-delà de MAX_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.

RouteRôle
GET /api/settings/mailRéglages effectifs, saved, configured, has_password ; jamais de mot de passe
PUT /api/settings/mailRemplacement complet : enabled, host, port, starttls, username, sender, timeout, et password facultatif, en écriture seule
DELETE /api/settings/mailRé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}/retryRemet 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é dans app_settings (clé exports), sans changement de schéma.
  • Livre (qui peut modifier le volume) : PATCH /api/projects/{pid}/edition avec {"ai_disclosure": true | false | null} ; null revient au choix de l’installation.
  • GET et PATCH /api/projects/{pid}/edition renvoient ai_disclosure (la valeur du prochain export : choix du livre, sinon celui de l’installation), ai_disclosure_default (celui de l’installation) et ai_disclosure_book (choix propre au livre, null = suit l’installation). Une interface affiche ai_disclosure et 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 cheminCorps 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.