{
  "openapi": "3.0.3",
  "info": {
    "title": "tendre.AI API Pro",
    "version": "1.0",
    "description": "Rendu cloud tendre.AI (images, vidéos, personnages entraînés) piloté par un serveur ou un agent, avec les crédits du compte Pro. Guide : API-PRO.md. Les prix cités ici sont ceux du jour (cette spécification est servie par le proxy avec sa grille courante) ; grille complète sur GET /pricing, prix exact d'un rendu sur POST /videos/quote et POST /images/quote. Chaque image et vidéo livrée porte dans ses métadonnées XMP la mention IPTC Digital Source Type trainedAlgorithmicMedia (générée par IA, article 50 du règlement européen sur l'IA), sans option pour la retirer. Connexion coupée avant la réponse d'une soumission (délai d'un proxy, erreur 524) : le rendu n'est pas lancé ou il est annulé aussitôt, rien n'est débité (lot compris) ; relancer la demande."
  },
  "servers": [
    {
      "url": "https://api.tendre.ai/pro/v1"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Clé Pro : Authorization: Bearer tk_pro_..."
      }
    },
    "schemas": {
      "Erreur": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "Soumission": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string"
          },
          "seed": {
            "type": "integer"
          },
          "cost": {
            "type": "number",
            "description": "Crédits débités à la livraison"
          }
        }
      },
      "Lot": {
        "type": "object",
        "description": "Réponse de POST /images quand count > 1",
        "properties": {
          "count": {
            "type": "integer",
            "description": "Rendus effectivement créés"
          },
          "requested": {
            "type": "integer"
          },
          "cost_total": {
            "type": "number"
          },
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Soumission"
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "jobId": {
            "type": "string",
            "description": "Premier rendu",
            "pour compatibilité": null
          },
          "seed": {
            "type": "integer"
          },
          "cost": {
            "type": "number"
          }
        }
      },
      "Etat": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "completed",
              "failed"
            ]
          },
          "phase": {
            "type": "string",
            "description": "IN_QUEUE ou IN_PROGRESS quand pending"
          },
          "cost": {
            "type": "number"
          },
          "file_url": {
            "type": "string",
            "description": "Chemin du fichier quand completed"
          },
          "detail": {
            "type": "string",
            "description": "Cause quand failed"
          },
          "reason": {
            "type": "string",
            "description": "Ultra à la seconde refusé à la sortie : le code du moteur (ex. OutputVideoSensitiveContentDetected) ; rien n'est débité"
          }
        }
      },
      "Devis": {
        "type": "object",
        "description": "Prix calculé sans rien soumettre ni réserver",
        "properties": {
          "cost": {
            "type": "number",
            "description": "Crédits qui seraient débités"
          },
          "mode": {
            "type": "string"
          },
          "format": {
            "type": "string",
            "description": "480p, 720p ou 1080p (vidéo)"
          },
          "audio": {
            "type": "boolean"
          },
          "fps": {
            "type": "integer",
            "description": "Cadence retenue (vidéo), en images par seconde"
          },
          "duration_auto": {
            "type": "boolean"
          },
          "method": {
            "type": "string",
            "description": "Créer une image (multiref) : simple, zones, retouche ou melange"
          },
          "method_detail": {
            "type": "string",
            "description": "Créer une image : ce que fait la méthode"
          },
          "passes": {
            "type": "integer",
            "description": "Créer une image : rendus enchaînés"
          },
          "billed_passes": {
            "type": "integer",
            "description": "Créer une image : passes facturées (une passe = le prix d'une image)"
          },
          "seconds_max": {
            "type": "number",
            "description": "Durée maximale en secondes (vidéo), à cette cadence : la durée demandée en durée fixe ; en durée automatique, 481 images au plus (20 à 24 i/s, 16 à 30, 8 à 60 ; 20 en consistency enhanced)"
          },
          "seconds": {
            "type": "number",
            "description": "Ultra à la seconde : la durée facturée, en secondes (4 à 30)"
          },
          "extend_seconds": {
            "type": "number",
            "description": "Prolongation en Ultra : les secondes facturées de la vidéo prolongée (sa durée, à 24 i/s ; une cadence plus haute compte au prorata)"
          },
          "extend_direction": {
            "type": "string",
            "enum": [
              "forward",
              "backward"
            ],
            "description": "Prolongation en Ultra : le sens demandé"
          },
          "detail": {
            "type": "string",
            "description": "D'où vient le chiffre, ex. forfait 5 x 3 (1080p) x 2 (son) = 30"
          }
        }
      },
      "Stockage": {
        "type": "object",
        "description": "Quota de LoRA de la licence, images et vidéos confondues",
        "properties": {
          "count": {
            "type": "integer"
          },
          "max_count": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "max_bytes": {
            "type": "integer"
          }
        }
      },
      "LoraInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "À passer dans lora"
          },
          "trigger": {
            "type": "string",
            "nullable": true,
            "description": "Mot déclencheur à reprendre dans le prompt"
          },
          "base": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          }
        }
      },
      "Lora": {
        "type": "object",
        "properties": {
          "lora": {
            "type": "string",
            "description": "Nom du personnage entraîné (voir GET /loras)"
          },
          "lora_strength": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "lora2": {
            "type": "string"
          },
          "lora2_strength": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          }
        }
      },
      "ImageIn": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Lora"
          },
          {
            "type": "object",
            "required": [
              "prompt"
            ],
            "properties": {
              "prompt": {
                "type": "string"
              },
              "width": {
                "type": "integer",
                "default": 1024
              },
              "height": {
                "type": "integer",
                "default": 1024
              },
              "steps": {
                "type": "integer"
              },
              "mode": {
                "type": "string",
                "enum": [
                  "edit",
                  "img2img"
                ]
              },
              "image_b64": {
                "type": "string",
                "description": "Image source en base64 (edit",
                "img2img)": null
              },
              "mask_b64": {
                "type": "string",
                "description": "Masque en base64"
              },
              "count": {
                "type": "integer",
                "minimum": 1,
                "maximum": 8,
                "default": 1,
                "description": "Nombre d'images en une requête, un seed neuf chacune (avec seed : seed, seed+1, seed+2...) ; la réponse porte alors jobs[] (Lot)"
              },
              "seed": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295,
                "description": "Graine du rendu ; absente, elle est tirée au hasard et rendue dans la réponse. Même seed, même prompt et mêmes réglages : le même rendu, à de très légers écarts près si la carte graphique change. Hors plage : 400 invalid_seed"
              },
              "multiref": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/MultirefImage"
                  }
                ],
                "description": "Créer une image avec des personnages, des objets et un lieu (schéma MultirefImage, guide 2.6) ; count 1 ou 2 (rendues l'une après l'autre), largeur et hauteur de 256 à 2048"
              }
            }
          }
        ]
      },
      "MultirefImage": {
        "type": "object",
        "description": "Créer une image (guide 2.6) : des personnages (GET /characters, par leur vue ou leur LoRA d'image), des objets et animaux (GET /subjects) et un lieu dans UNE image, désignés par @Nom dans le prompt anglais. 4 sujets au plus ; par images seules, 4 références au plus lieu compris. Le serveur choisit la méthode et enchaîne les passes ; une passe coûte le prix d'une image ; le total est réservé à la soumission et débité une fois, à l'image finale (un échec ou une annulation ne coûte rien). Réponse 202, jobId mri_..., suivi par GET /images/{jobId} (progress : passe en cours). Ouvert quand GET /health dit multirefImage, sinon 503 multiref_image_disabled.",
        "properties": {
          "characters": {
            "type": "array",
            "maxItems": 4,
            "items": {
              "type": "object",
              "required": [
                "character_id",
                "name"
              ],
              "properties": {
                "character_id": {
                  "type": "string"
                },
                "name": {
                  "type": "string",
                  "description": "Un mot (lettres, chiffres, _), celui écrit après @ dans le prompt"
                },
                "view": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 5,
                  "default": 1,
                  "description": "La vue de sa sheet qui le représente (1 = buste de face) ; absente de la sheet : 400 invalid_view"
                },
                "sheet_id": {
                  "type": "string",
                  "description": "Facultatif : la sheet à utiliser (la plus récente par défaut)"
                },
                "lora": {
                  "type": "string",
                  "description": "Facultatif : son LoRA d'image (liste image de GET /loras) ; inconnu : 403 unknown_lora"
                },
                "strength": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1.5,
                  "default": 1,
                  "description": "Force du LoRA"
                },
                "lora_pass": {
                  "type": "number",
                  "minimum": 0.3,
                  "maximum": 0.9,
                  "default": 0.55,
                  "description": "Part repeinte par sa passe LoRA"
                },
                "description": {
                  "type": "string",
                  "description": "Facultatif, en anglais : tenue, âge..."
                }
              }
            }
          },
          "subjects": {
            "type": "array",
            "maxItems": 4,
            "items": {
              "type": "object",
              "required": [
                "subject_id"
              ],
              "properties": {
                "subject_id": {
                  "type": "string"
                },
                "name": {
                  "type": "string",
                  "description": "Un mot ; par défaut le premier mot de son nom"
                },
                "description": {
                  "type": "string"
                },
                "image": {
                  "type": "integer",
                  "minimum": 1,
                  "default": 1,
                  "description": "Laquelle de ses images"
                }
              }
            }
          },
          "location": {
            "type": "object",
            "properties": {
              "subject_id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "description": {
                "type": "string"
              }
            },
            "description": "Un lieu enregistré (type location)"
          },
          "background_b64": {
            "type": "string",
            "description": "Décor ponctuel (JPEG, PNG ou WebP, 12 Mo au plus), à la place de location"
          },
          "background_description": {
            "type": "string"
          }
        }
      },
      "Sequence": {
        "type": "object",
        "properties": {
          "sequence_id": {
            "type": "string"
          },
          "clips": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "job_id": {
                  "type": "string"
                },
                "stored": {
                  "type": "boolean"
                }
              }
            }
          },
          "resolution": {
            "type": "string",
            "nullable": true
          },
          "exported": {
            "type": "boolean"
          },
          "seconds": {
            "type": "number",
            "nullable": true
          },
          "file_url": {
            "type": "string",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FaceSwapIn": {
        "type": "object",
        "required": [
          "video_b64",
          "character_id"
        ],
        "description": "Face swap (personnage entier) : la personne filmée devient un personnage (3.8), visage, cheveux, corps et tenue, en gardant ses gestes, sa bouche, sa voix et le décor. Le serveur prépare la vidéo et les vues ; une scène explicite prend les trois portraits recadrés à la tête et la tenue de la fiche n'y est plus imposée.",
        "properties": {
          "video_b64": {
            "type": "string",
            "description": "La vidéo à transformer en base64 (MP4, MOV, WebM ou MKV), 20 Mo au plus : la couper ou la compresser avant, ou passer par l'outil MCP tendre_face_swap (95 Mo)"
          },
          "character_id": {
            "type": "string",
            "description": "Un personnage de GET /characters"
          },
          "sheet_id": {
            "type": "string",
            "description": "Sa sheet, s'il en a plusieurs (la plus récente par défaut)"
          },
          "name": {
            "type": "string",
            "maxLength": 60,
            "description": "Son nom, par défaut celui de sa fiche. Un personnage créé dans l'application n'en a pas côté serveur (name null dans GET /characters) : le donner pour l'appeler par @Nom dans scene, sinon l'appeler Image 1"
          },
          "start": {
            "type": "number",
            "minimum": 0,
            "default": 0,
            "description": "Début du passage, en secondes"
          },
          "duration": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 20,
            "description": "Durée du passage, en secondes ; absente : jusqu'à la fin, 20 s au plus. Le passage se compte en images : 30 i/s pour une source à 29 i/s ou plus (30, 50, 60), sinon 24, forme 8n+1"
          },
          "scene": {
            "type": "string",
            "maxLength": 400,
            "description": "Facultative, EN ANGLAIS : ce qui doit changer ou être précisé ; le personnage s'y désigne par @Nom (le premier mot de son nom) ou Image 1. Sans scène, il bouge exactement comme la personne filmée. Jamais de parole, de bouche, de regard ni de tête (says, looks at the camera, smiles) : il les recopie de la vidéo, une consigne les lui fait inventer et décale les lèvres. Écrire ce qui change autour : tenue, accessoire, lumière"
          },
          "description": {
            "type": "string",
            "maxLength": 360,
            "description": "Facultative, en anglais : qui il est ; par défaut sa fiche (sa nature, et sa tenue hors scène explicite)"
          },
          "part": {
            "type": "string",
            "enum": [
              "full",
              "head"
            ],
            "default": "full",
            "description": "full : le personnage entier (visage, cheveux, corps, tenue) ; head : seulement la tête (visage et cheveux), le corps filmé reste, à partir des trois portraits du personnage recadrés à la tête"
          },
          "zone": {
            "type": "string",
            "enum": [
              "tight",
              "normal",
              "wide"
            ],
            "description": "La place autour de la silhouette filmée (ou de la tête en part head) où le personnage peut être dessiné : wide pour de grandes oreilles, des ailes ou une grande carrure. Par défaut normal, tight en part head"
          },
          "likeness": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 2,
            "default": 1,
            "description": "Ressemblance à sa fiche"
          },
          "audio": {
            "type": "boolean",
            "default": true,
            "description": "Garder le son de la vidéo (lèvres calées dessus) ; false : vidéo muette"
          },
          "voice": {
            "type": "string",
            "enum": [
              "character",
              "original"
            ],
            "default": "character",
            "description": "character : le son de la vidéo est converti dans la VOIX DE LA FICHE du personnage quand elle en a une en ligne (envoyée par l'application), dans le même rendu, au rythme d'origine ; original : garder la voix filmée. voice_b64 passe devant. La réponse dit laquelle a servi (voice : character, custom, original ou none)"
          },
          "voice_b64": {
            "type": "string",
            "description": "Facultative, avec audio true : une autre voix pour le personnage (WAV, MP3, OGG, FLAC, 2 Mo au plus, 20 premières secondes) ; le son de la vidéo est converti dans cette voix. Le corps entier de la requête ne dépasse pas 30 Mo : vidéo et voix ensemble, 22 Mo au plus avant base64"
          },
          "seed": {
            "type": "integer",
            "minimum": 0,
            "maximum": 4294967295
          }
        }
      },
      "VideoIn": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Lora"
          },
          {
            "type": "object",
            "required": [
              "prompt"
            ],
            "properties": {
              "prompt": {
                "type": "string",
                "description": "En anglais : l'action, l'ambiance, la lumière. Règles d'écriture (les mêmes que le bouton IA de l'application et les outils vidéo du MCP) : guide 5.3, « Écrire une vidéo comme un réalisateur »"
              },
              "width": {
                "type": "integer",
                "default": 720,
                "description": "Forfait par format, indépendant de la durée : 480p (petit côté 480) 5 crédits (2 sans son en Qualité, hors Multi-Ref et consistency enhanced), 720p 10, 1080p 15 ; multiplié par 2 avec le son. 1080p (1920 x 1088) au plus, Ultra compris : au-delà, 400 resolution_above_quality (plus de 4K depuis la 0.8.8). Grille : GET /pricing ; devis exact : POST /videos/quote"
              },
              "height": {
                "type": "integer",
                "default": 1280
              },
              "duration_auto": {
                "type": "boolean",
                "default": true,
                "description": "Le modèle choisit la durée d'après le texte, jusqu'à 481 images (20 s à 24 i/s, 16 s à 30, 8 s à 60), et length est ignoré ; false = durée fixe donnée par length. En consistency enhanced au-delà de 30 i/s, durée fixe obligatoire (400 fps_requires_fixed_duration)"
              },
              "length": {
                "type": "integer",
                "default": 121,
                "description": "Durée en IMAGES à la cadence fps, forme 8n+1 ; durée = (length - 1) / fps. 481 au plus (601 à 30 i/s en Qualité sans end_image et en Ultra jusqu'au 1080p ; à 60 i/s en Qualité sans end_image, 961 avec son et 1201 sans son ; 721 en consistency enhanced, 961 à 60 i/s sauf avec du son et des keyframes ou une end_image ; prix du format ×2 au-delà de 481 images ; au-delà du plafond, ramenée au plafond). À 24 i/s : 73, 121, 241, 361, 481 (20 s) ; à 30 i/s, 481 = 16 s ; à 60 i/s, 481 = 8 s. Prise en compte en durée fixe, ignorée en durée automatique"
              },
              "fps": {
                "type": "integer",
                "enum": [
                  24,
                  30,
                  50,
                  60
                ],
                "default": 30,
                "description": "Cadence en images par seconde. 30 par défaut (mouvements nets, conseillé), 60 le plus fluide, 24 la cadence du cinéma ; 50 en consistency enhanced seulement : léger ralenti (environ 0,83x). Le prix ne change pas (forfait, paliers comptés en images). Une prolongation garde la cadence du clip précédent (400 extend_fps_mismatch), une séquence celle de son premier plan (409 sequence_fps_mismatch). Cadence absente du mode : 400 invalid_fps. Avec du son (audio, et l'Ultra qui le rend par défaut) : les mêmes cadences, sauf 50 (ralenti de consistency enhanced, son pas encore vérifié : 400 audio_fps_unavailable)"
              },
              "consistency": {
                "type": "string",
                "enum": [
                  "standard",
                  "enhanced"
                ],
                "default": "standard",
                "description": "enhanced = cohérence renforcée : mouvement, visages et décor plus stables sur la durée, jusqu'à 721 images par plan (length 721, durée fixe : 30 s à 24 i/s, 24 s à 30 ; à 60 i/s, 961 images, 16 s, pour un plan neuf, 721 avec du son et des keyframes ou une end_image), premier plan comme prolongation ; une prolongation enhanced accepte aussi keyframes (at depuis la première image nouvelle) et end_image. Mode Qualité seulement (ni Ultra, ni Multi-Ref : 400 consistency_mode). Prix d'un clip du format jusqu'à 481 images, ×2 au-delà. Une séquence garde la même consistency (409 consistency_mismatch). Contenu explicite : pas encore en enhanced ; un plan seul de 20 s au plus (15 s avec end_image) passe en standard, sinon 400 consistency_explicit"
              },
              "mode": {
                "type": "string",
                "enum": [
                  "ltx25",
                  "turbo"
                ],
                "default": "ltx25",
                "description": "ltx25 = moteur 2.5 (son, 1080p). turbo : RETIRÉ le 11 octobre 2026, une demande turbo est rendue en Qualité 480p (même orientation, même durée, 241 images au plus, durée fixe), au forfait de la Qualité 480p"
              },
              "audio": {
                "type": "boolean",
                "default": false,
                "description": "Son généré avec l'image. Multiplie le forfait du clip par 2 : 480p 5 -> 10 (sans son, la Qualité 480p coûte 2), 720p 10 -> 20, 1080p 15 -> 30"
              },
              "image": {
                "type": "string",
                "description": "Image de départ en base64"
              },
              "end_image": {
                "type": "string",
                "description": "Image d'arrivée en base64 (transition). La transition sort à la taille demandée, du 480p au 1080p, et se paie comme un clip de ce format"
              },
              "end_image_strength": {
                "type": "number",
                "minimum": 0.3,
                "maximum": 1,
                "description": "Fermeté de l'arrivée, avec end_image : 0,4 souple (changement réparti, image de fin approchée), 0,75 normale, 1 ferme (image de fin exacte). Absente : 0,75, et 1 en consistency enhanced. Sans effet sur le prix."
              },
              "keyframes": {
                "type": "array",
                "maxItems": 4,
                "description": "Images clés (mode Qualité) : le clip montre chaque image à son instant. Durée fixe (length) ; 0 < at < durée, 0,5 s d'écart au moins ; même prix qu'un clip du même format ; pas en Ultra ni en Multi-Ref ; en prolongation, seulement en consistency enhanced.",
                "items": {
                  "type": "object",
                  "required": [
                    "image",
                    "at"
                  ],
                  "properties": {
                    "image": {
                      "type": "string",
                      "description": "Image en base64"
                    },
                    "at": {
                      "type": "number",
                      "description": "Instant en secondes, 0 < at < durée du clip"
                    },
                    "strength": {
                      "type": "number",
                      "minimum": 0.1,
                      "maximum": 1,
                      "default": 1,
                      "description": "Force de l'image clé"
                    }
                  }
                }
              },
              "quality": {
                "type": "string",
                "enum": [
                  "max"
                ],
                "description": "Mode Ultra, le rendu le plus fin (guide 3.7). À LA SECONDE quand GET /pricing dit ultra.per_second_open : 4 crédits la seconde en 480p, 8 en 720p, 20 en 1080p, son compris, 4 à 30 s à 24 i/s (5 s en durée automatique ; en durée fixe, length / fps, fps 24 s'il est absent), 480p rendu en 480p, au-delà du 1080p 400 resolution_outside_ultra ; image de départ possible ; refus du moteur 400 engine_refused ; seed ignorée ; une fois en calcul, l'annulation répond 409 render_in_progress (livré et facturé). À PRIX FIXE sinon, et avec un LoRA ou en prolongation : 40 crédits en 720p, 60 en 1080p (une demande sous le 720p est rendue et facturée en 720p). Pas de 4K, pas de transition"
              },
              "extend_job_id": {
                "type": "string",
                "description": "Prolongation : identifiant du clip précédent ; le plan continue son mouvement et reprend sa résolution et sa cadence (ltx25) ; avec audio, il continue aussi le son du clip (sauf en consistency enhanced et en Ultra) ; length = images nouvelles (jusqu'à 721 en consistency enhanced) ; pas d'image d'arrivée sauf en consistency enhanced"
              },
              "extend_frames": {
                "type": "integer",
                "enum": [
                  9,
                  17,
                  25,
                  33,
                  41,
                  49
                ],
                "default": 25,
                "description": "Longueur de la fin du clip qui guide le plan suivant, en images (25 = environ 1 s à 24 i/s)"
              },
              "extend_video": {
                "type": "string",
                "description": "Prolongation sans extend_job_id : la fin d'un clip en MP4 base64, exactement extend_frames images, 8 Mo au plus ; si elle porte une piste son et que audio vaut true, le plan continue ce son (sauf en consistency enhanced et en Ultra)"
              },
              "extend_upload_id": {
                "type": "string",
                "description": "Prolongation en Ultra (quality max) d'un plan de plus de 24 h : l'identifiant rendu par POST /extensions, une fois le fichier ORIGINAL du plan déposé tel que livré. Le rendu prend la résolution de cette vidéo (width et height ignorés) ; une seule utilisation"
              },
              "extend_direction": {
                "type": "string",
                "enum": [
                  "forward",
                  "backward"
                ],
                "default": "forward",
                "description": "Prolongation en Ultra (extend_job_id d'un rendu Ultra ou extend_upload_id) : forward rend la suite du plan, backward les instants juste avant lui (à monter devant). Le rendu ne contient que la partie nouvelle (seconds ou length/fps, 5 s en durée automatique), à la résolution du plan, sans image de départ ni références. Prix : la partie nouvelle au prix de l'Ultra, plus 2 crédits par seconde de vidéo prolongée en 480p, 5 en 720p, 13 en 1080p. backward hors séquence"
              },
              "sequence_id": {
                "type": "string",
                "description": "Range ce plan dans une séquence (POST /sequences) à la fin de son rendu"
              },
              "seed": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295,
                "description": "Graine du rendu ; absente, elle est tirée au hasard et rendue dans la réponse. Même seed, même prompt, même image et mêmes réglages (format, durée, son, mode) : le même clip, à de légers écarts près si la carte graphique change. Hors plage : 400 invalid_seed"
              },
              "multiref": {
                "$ref": "#/components/schemas/Multiref"
              }
            }
          }
        ]
      },
      "Multiref": {
        "type": "object",
        "description": "Multi-Ref Vidéo (guide 3.9) : 1 à 4 références, personnages (GET /characters) et objets ou animaux enregistrés (GET /subjects) confondus, désignées par @Nom dans le prompt anglais ; un lieu enregistré (location) ou un décor ponctuel (background_b64), pas les deux. Formats : 480x832, 720x1280, 1088x1920, 832x480, 1280x720, 1920x1088, 480x480, 720x720, 1088x1088 (480p en Qualité seulement, pas de 4K). Qualité ou Ultra (quality max, 1080p à prix fixe propre au Multi-Ref), pas de turbo, de prolongation ni de transition ; en durée fixe, length = 5, 10 ou 20 s en images à la cadence fps (481 au plus ; 121, 241, 481 à 24 i/s), sinon 400 invalid_length. Réponse : warnings limited_likeness pour un personnage d'une seule vue. MULTI-REF ULTRA À LA SECONDE quand GET /pricing dit ultra.multiref_per_second_open : 4 crédits la seconde en 480p, 8 en 720p, 20 en 1080p, son compris, 4 à 30 s à 24 i/s (5 s en durée automatique), 480p à 1080p ; chaque personnage part avec sa character sheet assemblée en une planche, chaque objet ou lieu avec ses images ; le premier rendu d'un personnage dépose sa planche (jobId sdp-...) ; voix ignorées (warnings voice_ignored), strength sans effet",
        "properties": {
          "characters": {
            "type": "array",
            "maxItems": 4,
            "items": {
              "type": "object",
              "required": [
                "character_id",
                "name"
              ],
              "properties": {
                "character_id": {
                  "type": "string"
                },
                "name": {
                  "type": "string",
                  "pattern": "^[\\p{L}\\p{N}_]{1,30}$",
                  "description": "Un mot, celui écrit après @ dans le prompt"
                },
                "description": {
                  "type": "string",
                  "maxLength": 300,
                  "description": "Courte description en anglais ; par défaut, le kind du personnage"
                },
                "strength": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 2,
                  "default": 1
                },
                "sheet_id": {
                  "type": "string",
                  "description": "Facultatif : la sheet a utiliser (la plus recente par defaut)"
                },
                "voice": {
                  "type": "string",
                  "enum": [
                    "character",
                    "generated"
                  ],
                  "default": "character",
                  "description": "Avec audio true, characters[0] et characters[1] : character = la VOIX DE SA FICHE quand elle est en ligne (envoyée par l'application ; has_voice sur le personnage), generated = une voix faite par le modèle. voice_b64 passe devant. La réponse dit laquelle a servi (voices)"
                },
                "voice_b64": {
                  "type": "string",
                  "description": "Facultatif, characters[0] et characters[1] seulement, avec audio true : un fichier audio (WAV, MP3, OGG, FLAC) en base64, 4 Mo au plus (un extrait court suffit), la voix du personnage ; le serveur en prend les 5 premières secondes et le modèle fait parler le personnage avec cette voix. Rangez les personnages dans l'ordre du dialogue : characters[0] parle en premier"
                }
              }
            }
          },
          "subjects": {
            "type": "array",
            "maxItems": 4,
            "description": "Objets et animaux enregistrés, dans les emplacements avec les personnages (4 au plus en tout)",
            "items": {
              "$ref": "#/components/schemas/MultirefSubject"
            }
          },
          "location": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MultirefSubject"
              }
            ],
            "description": "Un lieu enregistré (type location) comme décor : toutes ses images en Ultra, les 5 premières sinon, une force propre. Remplace background_b64"
          },
          "styles": {
            "type": "array",
            "description": "Ultra seulement (quality max, hors scène adulte) : styles enregistrés (type style). Toute la vidéo prend leur rendu (technique, lumière, couleurs, textures), jamais leur contenu. La description dit la TECHNIQUE en anglais, ex. « loose watercolor painting on textured paper ». 30 images au plus en tout (characters, subjects, styles, outfits, location)",
            "items": {
              "$ref": "#/components/schemas/MultirefSubject"
            }
          },
          "outfits": {
            "type": "array",
            "description": "Ultra seulement : tenues enregistrées (type outfit), portées telles que sur leurs images. Qui la porte : worn_by, ou dans le prompt (« @Lea wears @RedGown »)",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/MultirefSubject"
                }
              ],
              "properties": {
                "worn_by": {
                  "type": "string",
                  "description": "Facultatif : le name du personnage (characters) qui porte la tenue ; ajoute « <Nom> wears <Tenue>. » en tête de la scène"
                }
              }
            }
          },
          "motion": {
            "type": "object",
            "description": "Ultra seulement : UNE vidéo de mouvement de 4 à 30 s, 30 s au total avec les sons de référence (rapport largeur/hauteur 0,4 à 2,5 ; hors limites : 400 input_video_out_of_limits) ; le clip en reprend le mouvement de caméra (mode camera) ou la chorégraphie (mode choreography), jamais la personne ni le lieu. Le serveur la ramène en 480p sans son (MP4 H.264). Prix : crédits par seconde de mouvement en plus du rendu (GET /pricing, ultra.motion_per_second ; null : pas encore ouvert, 503 motion_not_open)",
            "properties": {
              "motion_id": {
                "type": "string",
                "description": "Une vidéo déposée par POST /motions (une seule utilisation)"
              },
              "video_b64": {
                "type": "string",
                "description": "Ou la vidéo en base64 (MP4, MOV, WebM, MKV), 20 Mo au plus"
              },
              "mode": {
                "type": "string",
                "enum": [
                  "camera",
                  "choreography"
                ],
                "default": "camera",
                "description": "camera : le mouvement de caméra, le cadrage et le rythme ; choreography : les mouvements du corps, les gestes et leur rythme. Même prix"
              },
              "description": {
                "type": "string",
                "maxLength": 300,
                "description": "Le mouvement en quelques mots anglais, ex. « a low tracking shot from the side »"
              }
            }
          },
          "background_b64": {
            "type": "string",
            "description": "Décor facultatif : JPEG, PNG ou WebP en base64, 12 Mo au plus"
          },
          "background_description": {
            "type": "string",
            "maxLength": 300
          }
        }
      },
      "MultirefSubject": {
        "type": "object",
        "required": [
          "subject_id"
        ],
        "properties": {
          "subject_id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "pattern": "^[\\p{L}\\p{N}_]{1,40}$",
            "description": "Un mot, celui écrit après @ ; par défaut le premier mot du nom du sujet"
          },
          "description": {
            "type": "string",
            "maxLength": 300,
            "description": "En anglais ; par défaut celle du sujet (300 caractères au plus dans le prompt)"
          },
          "strength": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "default": 1
          }
        }
      },
      "SubjectIn": {
        "type": "object",
        "required": [
          "type",
          "name",
          "images_b64"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "location",
              "object",
              "animal",
              "style",
              "outfit"
            ]
          },
          "name": {
            "type": "string",
            "pattern": "^[\\p{L}\\p{N}_]{1,40}$",
            "description": "Un mot, celui écrit après @ dans le prompt"
          },
          "description": {
            "type": "string",
            "maxLength": 300,
            "description": "En anglais"
          },
          "images_b64": {
            "type": "array",
            "minItems": 1,
            "maxItems": 6,
            "items": {
              "type": "string"
            },
            "description": "JPEG, PNG ou WebP, 512 px au moins sur le grand côté, 12 Mo chacune au plus ; 28 Mo pour le corps entier, sinon ajouter les suivantes par POST /subjects/{id}/images"
          }
        }
      },
      "Subject": {
        "type": "object",
        "properties": {
          "subject_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "location",
              "object",
              "animal",
              "style",
              "outfit"
            ]
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "origin": {
            "type": "string",
            "enum": [
              "api",
              "app"
            ],
            "description": "app : sujet de la Bibliothèque de l'application (même licence)"
          },
          "images": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "n": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 6
                },
                "w": {
                  "type": "integer",
                  "nullable": true
                },
                "h": {
                  "type": "integer",
                  "nullable": true
                },
                "mime": {
                  "type": "string",
                  "nullable": true
                },
                "file_url": {
                  "type": "string"
                }
              }
            }
          },
          "complete": {
            "type": "boolean",
            "description": "false : toutes les images d'un sujet de l'application ne sont pas encore synchronisées (il sert avec celles qui sont là)"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          },
          "last_used_at": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "nullable": true,
            "description": "Effacement si le sujet reste inutilisé jusque-là (un an sans usage) ; null si rien ne sera effacé"
          },
          "generation": {
            "type": "object",
            "description": "Sujet généré (POST /subjects/generate) : l'état de son image de départ et de ses vues ; absent pour un sujet fait d'images",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "ready",
                  "partial",
                  "failed"
                ]
              },
              "start": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "ready",
                      "failed"
                    ]
                  },
                  "origin": {
                    "type": "string",
                    "enum": [
                      "generated",
                      "imported"
                    ]
                  },
                  "n": {
                    "type": "integer"
                  },
                  "error": {
                    "type": "string"
                  }
                }
              },
              "views": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "ready",
                        "failed"
                      ]
                    },
                    "n": {
                      "type": "integer",
                      "description": "Numéro de l'image dans images"
                    },
                    "error": {
                      "type": "string"
                    },
                    "custom_instructions": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "available_views": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "SubjectGenerateIn": {
        "type": "object",
        "required": [
          "type",
          "name"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "object",
              "location",
              "animal",
              "style",
              "outfit"
            ]
          },
          "name": {
            "type": "string",
            "pattern": "^[\\p{L}\\p{N}_]{1,40}$",
            "description": "Un mot, celui écrit après @ dans le prompt"
          },
          "description": {
            "type": "string",
            "maxLength": 300,
            "description": "En anglais : l'élément seul (forme, matières, couleurs, détails), sans décor ni personnage. Requis sauf avec prompt ou start_image_b64"
          },
          "prompt": {
            "type": "string",
            "maxLength": 2000,
            "description": "Le texte complet de l'image de départ, en anglais, à la place du texte automatique tiré de description"
          },
          "start_image_b64": {
            "type": "string",
            "description": "Image de départ importée (JPEG, PNG ou WebP, 512 px au moins), gardée telle quelle et gratuite"
          },
          "views": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "Clés des vues, dans l'ordre. object : three_quarter, side, back, top, detail ; location : reverse_shot, side, high_angle, night, detail ; animal : front, three_quarter, back, sitting, lying ; style : portrait, landscape, interior, object, animal ; outfit : three_quarter, side, back, detail"
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "maximum": 6,
            "description": "À la place de views : le nombre d'images en tout, départ compris (les premières vues de la liste)"
          },
          "seed": {
            "type": "integer",
            "minimum": 0,
            "maximum": 4294967295,
            "description": "Graine de l'image de départ"
          },
          "quote_only": {
            "type": "boolean",
            "description": "true : rend seulement le prix (images, cost), rien n'est créé"
          }
        }
      },
      "CharacterIn": {
        "type": "object",
        "required": [
          "name",
          "kind",
          "image_b64"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40
          },
          "kind": {
            "type": "string",
            "description": "woman, man, ou une créature décrite en anglais (ex. baby dragon)"
          },
          "image_b64": {
            "type": "string",
            "description": "La photo : JPEG, PNG ou WebP, 1024 px au moins sur le grand côté, 12 Mo au plus"
          },
          "photo_role": {
            "type": "string",
            "enum": [
              "bust",
              "full_body"
            ],
            "default": "bust",
            "description": "La photo devient la vue 1 (buste) ou 4 (en pied)"
          },
          "outfit": {
            "type": "string",
            "maxLength": 700,
            "description": "Tenue en anglais (700 caractères au plus, environ 100 mots) de la vue en pied prolongée d'une photo en buste ; une photo en pied fait foi, la tenue n'y est pas posée. Sans effet sur le prix"
          }
        }
      },
      "ExportProjet": {
        "type": "object",
        "properties": {
          "manifest": {
            "type": "object",
            "description": "Manifeste tendre.project/1 : schema, source (api), exported_at, project, assembled, shots[] (id, number, state ready|rendering|failed|expired, file, original, start, duration, fps, width, height, audio, quality, consistency, seed, cost, prompt, prompt_sent, settings, extends, direction, references[], render, api_request), timeline (null pour l'API), editing_files, references_included (true pour le ZIP, false en manifest_only : références sans fichier)"
          },
          "file_url": {
            "type": "string",
            "nullable": true,
            "description": "Lien du ZIP (null avec manifest_only)"
          },
          "export_id": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "nullable": true
          },
          "bytes": {
            "type": "integer"
          }
        }
      },
      "Character": {
        "type": "object",
        "properties": {
          "character_id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Personnage de l'application (origin app) : le nom de sa fiche, une fois envoyé par l'application (0.9.1 et suivantes), null avant"
          },
          "kind": {
            "type": "string",
            "nullable": true,
            "description": "woman, man ou une créature en anglais ; origin app : celui de la fiche, une fois envoyé par l'application, null avant"
          },
          "origin": {
            "type": "string",
            "enum": [
              "api",
              "app"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "partial",
              "failed",
              "empty"
            ]
          },
          "sheet_id": {
            "type": "string",
            "description": "Sheet servie (un personnage de l'application peut en avoir plusieurs ; la plus recente par defaut, ?sheet=<id> pour une autre)"
          },
          "sheets": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Toutes les sheets, quand il y en a plusieurs"
          },
          "sheet_name": {
            "type": "string",
            "description": "Sheet ajoutée par POST /characters/{id}/sheets seulement : son nom"
          },
          "sheet_outfit": {
            "type": "string",
            "nullable": true,
            "description": "Sheet ajoutée seulement : sa tenue"
          },
          "sheet_origin": {
            "type": "string",
            "enum": [
              "api"
            ],
            "description": "Présent quand la sheet servie a été ajoutée par l'API (POST /characters/{id}/sheets)"
          },
          "progress": {
            "type": "object",
            "properties": {
              "ready": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "has_voice": {
            "type": "boolean",
            "description": "La voix de sa fiche est en ligne (envoyée par l'application, ou une voix par défaut fournie par le serveur) : le Face swap et le Multi-Ref la prennent par défaut"
          },
          "views": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "role": {
                  "type": "integer",
                  "enum": [
                    1,
                    2,
                    3,
                    4,
                    5,
                    6,
                    7,
                    8,
                    9
                  ],
                  "description": "1 buste de face, 4 en pied de face, 5 en pied de dos, 6 en pied de profil (regard vers la gauche de l'image), 7 gros plan du visage, 8 gros plan de la tenue, 9 gros plan des pieds ; 2 et 3 (bustes de trois-quarts) : personnages faits avant le 2026-10-21 seulement"
                },
                "name": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "ready",
                    "failed",
                    "missing"
                  ]
                },
                "error": {
                  "type": "string"
                },
                "file_url": {
                  "type": "string"
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "nullable": true
          },
          "last_used_at": {
            "type": "string",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "nullable": true,
            "description": "Effacement si le personnage reste inutilisé jusque-là (un an sans usage) ; null si rien ne sera effacé"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "cost_estimate": {
            "type": "integer",
            "description": "Création, sheet ajoutée et vue refaite seulement"
          },
          "existing": {
            "type": "boolean",
            "description": "POST /characters seulement : la même création avait déjà été reçue, voici ce personnage, rien n'est débité en plus"
          }
        }
      }
    },
    "responses": {
      "NonAutorise": {
        "description": "Clé absente ou invalide",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Refuse": {
        "description": "Clé désactivée",
        "révoquée ou expirée": null,
        "droit manquant": null,
        "licence révoquée ou non Pro": null,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Plafond": {
        "description": "Plafond atteint (voir Retry-After) ou plafond de crédits du jour de la clé (daily_credit_cap)",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/images": {
      "post": {
        "summary": "Soumettre une image",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImageIn"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rendu accepté (Soumission), ou lot de rendus quand count > 1 (Lot)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Soumission"
                    },
                    {
                      "$ref": "#/components/schemas/Lot"
                    }
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Créer une image (multiref) : chaîne acceptée, jobId mri_... (avec count 2 : jobs et cost_total) ; method, passes, billed_passes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Soumission"
                }
              }
            }
          },
          "400": {
            "description": "Prompt absent ou contenu interdit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAutorise"
          },
          "402": {
            "description": "Crédits insuffisants",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Refuse"
          },
          "429": {
            "$ref": "#/components/responses/Plafond"
          },
          "503": {
            "description": "multiref_image_disabled : Créer une image n'est pas encore ouvert sur ce serveur ; storage_unavailable : lecture des vues d'un personnage refusée, réessayer"
          }
        }
      }
    },
    "/images/{jobId}": {
      "get": {
        "summary": "État d'une image",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "État",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Etat"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAutorise"
          }
        }
      }
    },
    "/images/{jobId}/file": {
      "get": {
        "summary": "Fichier PNG d'une image terminée",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Fichier indisponible"
          }
        }
      }
    },
    "/images/{jobId}/cancel": {
      "post": {
        "summary": "Annuler une image",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Annulé"
          }
        }
      }
    },
    "/videos": {
      "post": {
        "summary": "Soumettre une vidéo",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VideoIn"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rendu accepté",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Soumission"
                }
              }
            }
          },
          "400": {
            "description": "Prompt absent ou contenu interdit"
          },
          "401": {
            "$ref": "#/components/responses/NonAutorise"
          },
          "402": {
            "description": "Crédits insuffisants"
          },
          "403": {
            "$ref": "#/components/responses/Refuse"
          },
          "429": {
            "$ref": "#/components/responses/Plafond"
          },
          "503": {
            "description": "storage_unavailable : lecture des vues d'un personnage refusée, réessayer"
          }
        }
      }
    },
    "/videos/{jobId}": {
      "get": {
        "summary": "État d'une vidéo",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "État",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Etat"
                }
              }
            }
          }
        }
      }
    },
    "/videos/{jobId}/file": {
      "get": {
        "summary": "Fichier MP4 d'une vidéo terminée",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "MP4",
            "content": {
              "video/mp4": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          }
        }
      }
    },
    "/videos/{jobId}/cancel": {
      "post": {
        "summary": "Annuler une vidéo",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Annulé, rien n'est débité"
          },
          "409": {
            "description": "render_in_progress : rendu Ultra à la seconde déjà en calcul, il ne s'arrête plus ; il est livré et facturé"
          }
        }
      }
    },
    "/videos/{jobId}/export": {
      "post": {
        "summary": "Exporter le dossier de projet d'un rendu vidéo (gratuit)",
        "description": "ZIP avec le manifeste tendre-project.json, les clips encore disponibles, l'original signé s'il existe, les vues des personnages et les images des éléments, timeline.otio et timeline.fcpxml. Fiche du rendu gardée 24 h, sans image ni vidéo ; ZIP effacé 45 min après le premier téléchargement, 2 h au plus. Voir API-PRO.md 3.6 bis.",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "manifest_only": {
                    "type": "boolean",
                    "description": "Le manifeste seul, sans ZIP"
                  },
                  "include_media": {
                    "type": "boolean",
                    "default": true,
                    "description": "Les vidéos des plans et leurs originaux signés (vrai par défaut). Faux : aucune vidéo, les plans gardent textes et réglages (state expired, videos_included false)"
                  },
                  "include_references": {
                    "type": "boolean",
                    "default": true,
                    "description": "Copier les références : vues des personnages, images des éléments (vrai par défaut). Faux : aucune, le manifeste garde leurs noms et @mentions (references_included false)"
                  },
                  "exclude_references": {
                    "type": "array",
                    "maxItems": 200,
                    "items": {
                      "type": "string"
                    },
                    "description": "Les références à NE PAS copier, une par une, par id (character_id, subject_id), nom ou @mention : references_included \"partial\", included false sur chacune. Une entrée inconnue : 400 unknown_reference"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Manifeste (tendre.project/1) et lien du ZIP",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportProjet"
                }
              }
            }
          },
          "404": {
            "description": "unknown_export : rendu inconnu, d'une autre licence ou soumis il y a plus de 24 h"
          },
          "409": {
            "description": "export_in_progress : un export a la fois par licence"
          },
          "413": {
            "description": "export_too_large : plus de 2 Go, exportez les plans un par un"
          }
        }
      }
    },
    "/exports/{id}/file": {
      "get": {
        "summary": "Télécharger le ZIP d'un dossier de projet",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ZIP",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "unknown_export : export inconnu ou effacé"
          }
        }
      }
    },
    "/loras": {
      "get": {
        "summary": "Personnages entraînés et LoRA déposés du compte, avec le quota de stockage",
        "responses": {
          "200": {
            "description": "Listes image et vidéo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "image": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LoraInfo"
                      }
                    },
                    "video": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LoraInfo"
                      }
                    },
                    "storage": {
                      "$ref": "#/components/schemas/Stockage"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/loras/upload": {
      "post": {
        "summary": "Déposer son propre LoRA en un envoi (jusqu'à 90 Mo ; au-delà, /loras/upload/start ; Krea 2 pour les images, LTX pour les vidéos, vérifié dans l'en-tête)",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nom du fichier .safetensors (lettres, chiffres, . _ -, 80 caractères au plus) ; un nom déjà pris est remplacé"
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "video"
              ],
              "default": "image"
            },
            "description": "image = Krea 2 (droit images), video = LTX 2.5 (droit videos)"
          },
          {
            "name": "trigger",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Mot déclencheur à reprendre dans le prompt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Déposé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "name": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string"
                    },
                    "size": {
                      "type": "integer"
                    },
                    "trigger": {
                      "type": "string",
                      "nullable": true
                    },
                    "architecture": {
                      "type": "string",
                      "description": "krea2, ltx ou inconnue"
                    },
                    "storage": {
                      "$ref": "#/components/schemas/Stockage"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Nom invalide"
          },
          "403": {
            "$ref": "#/components/responses/Refuse"
          },
          "411": {
            "description": "Content-Length absent"
          },
          "413": {
            "description": "Fichier trop gros (file_too_large) ou quota de la licence atteint (lora_quota, avec storage)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "415": {
            "description": "Pas un safetensors (invalid_safetensors) ou LoRA d'une autre architecture que Krea 2 / LTX (incompatible_lora, avec architecture) ; rien n'est stocké",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "503": {
            "description": "Stockage injoignable (upload_refused) : réessayer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "507": {
            "description": "Stockage plein côté serveur (storage_full) : réessayer plus tard",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        }
      }
    },
    "/loras/upload/start": {
      "post": {
        "summary": "Ouvrir un envoi en morceaux (au-delà de 90 Mo)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "size"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "image",
                      "video"
                    ],
                    "default": "image"
                  },
                  "size": {
                    "type": "integer",
                    "description": "Octets du fichier entier"
                  },
                  "trigger": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session ouverte",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "uploadId": {
                      "type": "string"
                    },
                    "part_size": {
                      "type": "integer"
                    },
                    "max_part_size": {
                      "type": "integer"
                    },
                    "parts_expected": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "file_too_large (800 Mo) ou lora_quota",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        }
      }
    },
    "/loras/upload/part": {
      "post": {
        "summary": "Envoyer le morceau N (corps brut ; le morceau 1 porte l'en-tête, jugé ici)",
        "parameters": [
          {
            "name": "uploadId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "part",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Morceau reçu",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "part": {
                      "type": "integer"
                    },
                    "etag": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "unknown_upload_session"
          },
          "413": {
            "description": "part_too_large (100 Mo)"
          },
          "415": {
            "description": "invalid_safetensors ou incompatible_lora sur le morceau 1 ; session annulée",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        }
      }
    },
    "/loras/upload/finish": {
      "post": {
        "summary": "Assembler les morceaux et enregistrer le LoRA",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "uploadId",
                  "parts"
                ],
                "properties": {
                  "uploadId": {
                    "type": "string"
                  },
                  "parts": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "part": {
                          "type": "integer"
                        },
                        "etag": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "trigger": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Déposé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "name": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string"
                    },
                    "size": {
                      "type": "integer"
                    },
                    "trigger": {
                      "type": "string",
                      "nullable": true
                    },
                    "architecture": {
                      "type": "string"
                    },
                    "storage": {
                      "$ref": "#/components/schemas/Stockage"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "unknown_upload_session"
          }
        }
      }
    },
    "/loras/upload/abort": {
      "post": {
        "summary": "Renoncer à un envoi en morceaux",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "uploadId"
                ],
                "properties": {
                  "uploadId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Annulé"
          }
        }
      }
    },
    "/loras/{name}": {
      "delete": {
        "summary": "Supprimer un LoRA du compte et libérer le quota",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "video"
              ],
              "default": "image"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Supprimé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "name": {
                      "type": "string"
                    },
                    "storage": {
                      "$ref": "#/components/schemas/Stockage"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "LoRA absent du compte (unknown_lora)"
          }
        }
      }
    },
    "/credits": {
      "get": {
        "summary": "Solde, plafonds et rendus en cours",
        "responses": {
          "200": {
            "description": "Compte",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance": {
                      "type": "number",
                      "nullable": true
                    },
                    "unlimited": {
                      "type": "boolean",
                      "description": "Licence de développement : pas de solde, rien n'est débité"
                    },
                    "limits": {
                      "type": "object"
                    },
                    "running": {
                      "type": "integer"
                    },
                    "buy_url": {
                      "type": "string",
                      "description": "Lien d'achat de crédits de la licence (le paiement se fait sur la page, par l'utilisateur)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/credits/purchase": {
      "post": {
        "summary": "Acheter des crédits sur la carte enregistrée (si le titulaire a coché « mon agent peut acheter »)",
        "description": "Le paiement part sur la carte enregistrée de la licence, dans le plafond mensuel fixé par le titulaire (page compte, Recharge automatique) ; un e-mail confirme chaque achat. Un achat à la fois. Tout refus rend buy_url, le lien d'achat ordinaire. 503 payment_unavailable : ne pas relancer tout de suite, un débit passé est crédité.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "pack"
                ],
                "properties": {
                  "pack": {
                    "type": "string",
                    "description": "Id de pack (s, m, l, xl, xxl, xxxl) ou montant libre c<crédits>, ex. c25000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acheté",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "credits": {
                      "type": "integer"
                    },
                    "eur": {
                      "type": "number"
                    },
                    "balance": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "unknown_pack"
          },
          "402": {
            "description": "confirmation_required ou payment_declined (buy_url)"
          },
          "409": {
            "description": "agent_not_allowed, no_saved_card, monthly_cap_reached, topup_in_progress ou purchase_in_progress (buy_url)"
          },
          "503": {
            "description": "payment_unavailable : ne pas relancer tout de suite"
          }
        }
      }
    },
    "/images/quote": {
      "post": {
        "summary": "Prix d'une image sans la soumettre (toujours 1 crédit(s) par image) ; avec multiref : le prix de Créer une image (passes x count), method et passes",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImageIn"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Devis",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Devis"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAutorise"
          }
        }
      }
    },
    "/sequences": {
      "post": {
        "summary": "Ouvrir une séquence (vidéo longue assemblée plan par plan)",
        "responses": {
          "200": {
            "description": "Séquence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sequence"
                }
              }
            }
          }
        }
      }
    },
    "/sequences/{id}": {
      "get": {
        "summary": "État d'une séquence",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Séquence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sequence"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Supprimer une séquence tout de suite",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Supprimée"
          }
        }
      }
    },
    "/sequences/{id}/clips": {
      "post": {
        "summary": "Ranger un rendu vidéo terminé dans la séquence",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "job_id"
                ],
                "properties": {
                  "job_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Séquence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sequence"
                }
              }
            }
          }
        }
      }
    },
    "/sequences/{id}/export": {
      "post": {
        "summary": "Assembler les plans en une seule vidéo (sans réencodage)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Séquence assemblée",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sequence"
                }
              }
            }
          }
        }
      }
    },
    "/sequences/{id}/project": {
      "post": {
        "summary": "Exporter le dossier de projet d'une séquence (tous ses plans, gratuit)",
        "description": "Comme /videos/{jobId}/export, pour tous les plans de la séquence bout à bout ; le film n'y est que si la séquence a déjà été assemblée (l'assemblage efface les plans : exporter avant d'assembler pour avoir les deux).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "manifest_only": {
                    "type": "boolean",
                    "description": "Le manifeste seul, sans ZIP"
                  },
                  "include_film": {
                    "type": "boolean",
                    "default": false,
                    "description": "Mettre aussi le film de la séquence dans le dossier, si elle est déjà assemblée (faux par défaut : assembled null ; demande include_media)"
                  },
                  "include_media": {
                    "type": "boolean",
                    "default": true,
                    "description": "Les vidéos des plans et leurs originaux signés (vrai par défaut). Faux : aucune vidéo, les plans gardent textes et réglages (state expired, videos_included false)"
                  },
                  "include_references": {
                    "type": "boolean",
                    "default": true,
                    "description": "Copier les références : vues des personnages, images des éléments (vrai par défaut). Faux : aucune, le manifeste garde leurs noms et @mentions (references_included false)"
                  },
                  "exclude_references": {
                    "type": "array",
                    "maxItems": 200,
                    "items": {
                      "type": "string"
                    },
                    "description": "Les références à NE PAS copier, une par une, par id (character_id, subject_id), nom ou @mention : references_included \"partial\", included false sur chacune. Une entrée inconnue : 400 unknown_reference"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Manifeste (tendre.project/1) et lien du ZIP",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportProjet"
                }
              }
            }
          },
          "404": {
            "description": "unknown_export : rendu inconnu, d'une autre licence ou soumis il y a plus de 24 h"
          },
          "409": {
            "description": "export_in_progress : un export a la fois par licence"
          },
          "413": {
            "description": "export_too_large : plus de 2 Go, exportez les plans un par un"
          }
        }
      }
    },
    "/sequences/{id}/file": {
      "get": {
        "summary": "Vidéo assemblée (MP4)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "MP4",
            "content": {
              "video/mp4": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          }
        }
      }
    },
    "/characters": {
      "get": {
        "summary": "Les personnages de la licence (ceux de l'application compris ; un personnage créé ici apparaît aussi dans l'application, et l'y supprimer l'efface ici)",
        "responses": {
          "200": {
            "description": "Liste",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "max": {
                      "type": "integer"
                    },
                    "characters": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Character"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Créer un personnage (7 vues) depuis une photo, travail asynchrone",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CharacterIn"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Fabrication lancée. La même demande renvoyée (même photo, name, kind, photo_role, outfit ; connexion coupée avant la réponse) rend le personnage déjà créé avec existing true et cost_estimate 0, sans nouveau débit, sauf si toutes ses vues générées ont échoué",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Character"
                }
              }
            }
          },
          "400": {
            "description": "Champ invalide ou photo trop petite"
          },
          "402": {
            "description": "Crédits insuffisants"
          },
          "409": {
            "description": "Plafond de personnages atteint, ou la même création en cours d'enregistrement (character_busy : la renvoyer quelques secondes plus tard)"
          },
          "415": {
            "description": "Photo illisible"
          }
        }
      }
    },
    "/characters/quote": {
      "post": {
        "summary": "Prix de la création d'un personnage (rien n'est créé)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "outfit": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Devis",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cost": {
                      "type": "integer"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/characters/{id}": {
      "get": {
        "summary": "État d'un personnage et de ses vues",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sheet",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Une sheet précise (sheet_id), pour un personnage qui en a plusieurs ; sans, la sheet servie par défaut"
          }
        ],
        "responses": {
          "200": {
            "description": "Personnage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Character"
                }
              }
            }
          },
          "404": {
            "description": "Personnage inconnu (effacé après un an sans usage, ou supprimé dans l'application)"
          }
        }
      },
      "delete": {
        "summary": "Effacer un personnage et ses vues",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Effacé"
          },
          "404": {
            "description": "Personnage inconnu"
          },
          "409": {
            "description": "Fabrication en cours"
          }
        }
      }
    },
    "/characters/{id}/views/{role}/file": {
      "get": {
        "summary": "Une vue du personnage (image)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "role",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9
              ]
            }
          },
          {
            "name": "sheet",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Une sheet précise (sheet_id), pour un personnage qui en a plusieurs ; sans, la sheet servie par défaut"
          }
        ],
        "responses": {
          "200": {
            "description": "Image : une vue en pied générée (4, 5 et 6) est en 2K, 1024 x 2048, le buste et les gros plans en 832 x 1216 (1216 x 832 pour les pieds) ; en PNG, ou en JPEG au-delà de 2,9 Mo ; la vue issue de la photo garde sa taille (2048 px au plus)",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Vue absente"
          }
        }
      }
    },
    "/characters/{id}/views/{role}/redo": {
      "post": {
        "summary": "Refaire une vue (1 crédit(s)) ; la vue de la photo d'origine ne se refait pas",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "role",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                4,
                5,
                6,
                7,
                8,
                9
              ]
            }
          },
          {
            "name": "sheet",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Une sheet précise (sheet_id), pour un personnage qui en a plusieurs ; sans, la sheet servie par défaut"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "description": "Personnage de l'application seulement"
                  },
                  "instruction": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Personnage créé par l'API, ou sheet ajoutée par l'API, seulement : une consigne en anglais, à l'impératif, ajoutée à la vue ; elle précise ce que la vue montre du personnage (côté, présence d'un détail), jamais une autre transformation"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Vue en fabrication",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Character"
                }
              }
            }
          },
          "409": {
            "description": "Vue de la photo, fabrication en cours, instruction sur un personnage de l'application (app_character), ou sheet importée en une seule image (single_image_sheet)"
          }
        }
      }
    },
    "/characters/{id}/views/{role}": {
      "put": {
        "summary": "Déposer sa propre image pour une vue (gratuit, aucune génération) ; personnage créé par l'API, ou sheet ajoutée par l'API, seulement",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "role",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                4,
                5,
                6,
                7,
                8,
                9
              ]
            }
          },
          {
            "name": "sheet",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Une sheet précise (sheet_id), pour un personnage qui en a plusieurs ; sans, la sheet servie par défaut"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "image_b64"
                ],
                "properties": {
                  "image_b64": {
                    "type": "string",
                    "description": "JPEG, PNG ou WebP, 1024 px au moins sur le grand côté, 12 Mo au plus ; devient la source préférée des vues refaites ensuite (sauf le dos)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Personnage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Character"
                }
              }
            }
          },
          "400": {
            "description": "Image trop petite"
          },
          "409": {
            "description": "Personnage de l'application (app_character) ou fabrication en cours"
          },
          "413": {
            "description": "Image trop lourde"
          },
          "415": {
            "description": "Image illisible"
          }
        }
      }
    },
    "/characters/{id}/sheets": {
      "post": {
        "summary": "Ajouter une sheet (une tenue) à un personnage existant, créé par l'API ou dans l'application",
        "description": "La nouvelle sheet a son sheet_id (sheet_origin api) ; le personnage garde ses autres sheets. Vues fournies (views) : déposées telles quelles, gratuites. Rôles manquants : générés, 1 crédit(s) par vue, depuis la vue en pied et le buste fournis (generate false pour n'en générer aucun). Sans views (ou sans vue en pied ni buste) : la tenue est mise sur la vue en pied du personnage (sinon son buste), qui devient celle de la nouvelle sheet, puis les autres vues partent d'elle. L'application rapatrie la sheet dans la fiche. Passez sheet_id à GET /characters/{id}?sheet=, à redo, à PUT d'une vue et au Multi-Ref (characters[].sheet_id).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "Nom de la sheet, ex. steampunk"
                  },
                  "outfit": {
                    "type": "string",
                    "maxLength": 700,
                    "description": "La tenue, en anglais (700 caractères au plus, environ 100 mots) ; obligatoire sans views"
                  },
                  "kind": {
                    "type": "string",
                    "description": "Personnage de l'application seulement : woman, man, ou une créature en anglais"
                  },
                  "views": {
                    "type": "object",
                    "description": "Facultatif : rôle (\"1\", \"4\" à \"9\") -> image en base64 (JPEG, PNG ou WebP, 1024 px au moins sur le grand côté, 12 Mo au plus)",
                    "additionalProperties": {
                      "type": "string"
                    }
                  },
                  "generate": {
                    "type": "boolean",
                    "default": true,
                    "description": "false : seulement les vues fournies, rien de généré"
                  },
                  "from_sheet_id": {
                    "type": "string",
                    "description": "Sans views : la sheet à habiller (sinon celle servie par défaut)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Sheet ajoutée, vues manquantes en fabrication (cost_estimate)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Character"
                }
              }
            }
          },
          "400": {
            "description": "name invalide, outfit_or_views_required, views_required, invalid_role, kind_required, image trop petite"
          },
          "402": {
            "description": "Crédits insuffisants"
          },
          "404": {
            "description": "Personnage inconnu"
          },
          "409": {
            "description": "Fabrication en cours (character_busy), ni vue en pied ni buste à reprendre (source_sheet_empty), ou sheet source importée en une seule image (single_image_sheet)"
          }
        }
      }
    },
    "/characters/{id}/sheets/{sheet}": {
      "delete": {
        "summary": "Effacer une sheet ajoutée par l'API (ses vues) ; le personnage et ses autres sheets restent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sheet",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Effacée"
          },
          "404": {
            "description": "Personnage inconnu, ou sheet qui n'a pas été ajoutée par l'API (unknown_sheet)"
          },
          "409": {
            "description": "Fabrication en cours"
          }
        }
      }
    },
    "/subjects": {
      "get": {
        "summary": "Les lieux, objets, animaux, styles et tenues de la licence (ceux de la Bibliothèque de l'application compris)",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "location",
                "object",
                "animal",
                "style",
                "outfit"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "max": {
                      "type": "integer"
                    },
                    "subjects": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Subject"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Enregistrer un lieu, un objet, un animal, un style ou une tenue depuis 1 à 6 images (gratuit)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubjectIn"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Enregistré",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subject"
                }
              }
            }
          },
          "400": {
            "description": "Champ invalide ou image refusée (index)"
          },
          "409": {
            "description": "Plafond de sujets atteint (max)"
          },
          "413": {
            "description": "Image trop lourde"
          }
        }
      }
    },
    "/subjects/{id}": {
      "get": {
        "summary": "Fiche d'un sujet",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sujet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subject"
                }
              }
            }
          },
          "404": {
            "description": "Sujet inconnu"
          }
        }
      },
      "put": {
        "summary": "Modifier un sujet (images_b64 remplace toutes les images)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "location",
                      "object",
                      "animal",
                      "style",
                      "outfit"
                    ]
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "images_b64": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 6,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sujet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subject"
                }
              }
            }
          },
          "404": {
            "description": "Sujet inconnu"
          }
        }
      },
      "delete": {
        "summary": "Effacer un sujet et ses images (ici et dans l'application)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Effacé"
          },
          "404": {
            "description": "Sujet inconnu"
          }
        }
      }
    },
    "/subjects/{id}/images": {
      "post": {
        "summary": "Ajouter une image à un sujet (6 au plus)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "image_b64"
                ],
                "properties": {
                  "image_b64": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sujet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subject"
                }
              }
            }
          },
          "400": {
            "description": "Image refusée, ou déjà 6 images"
          },
          "404": {
            "description": "Sujet inconnu"
          }
        }
      }
    },
    "/subjects/generate": {
      "post": {
        "summary": "(À venir, 0.9.1 : pas encore ouvert, 404) Générer un lieu, un objet, un animal, un style ou une tenue sans image : un départ tiré de la description, puis des vues (1 crédit par image rendue)",
        "description": "Comme la fenêtre Générer des Éléments de l'application. 6 images au plus, départ compris ; un départ importé est gratuit ; une image en échec n'est pas débitée. Les images partent une à la fois, un échec arrête la série. Suivi : GET /subjects/{id}, champ generation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubjectGenerateIn"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "quote_only : prix",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "images": {
                      "type": "integer"
                    },
                    "cost": {
                      "type": "integer"
                    },
                    "detail": {
                      "type": "string"
                    },
                    "views": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "start": {
                      "type": "string",
                      "enum": [
                        "generated",
                        "imported"
                      ]
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Sujet créé, génération en cours (cost_estimate)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subject"
                }
              }
            }
          },
          "400": {
            "description": "Champ invalide (description_required, invalid_views, invalid_count, too_many_images...) ou contenu interdit"
          },
          "402": {
            "description": "Crédits insuffisants"
          },
          "409": {
            "description": "Plafond de sujets atteint (max)"
          },
          "503": {
            "description": "Consignes des éléments absentes (generation_unavailable) ou stockage indisponible"
          }
        }
      }
    },
    "/subjects/{id}/views": {
      "post": {
        "summary": "(À venir, 0.9.1) Ajouter des vues à un sujet généré (1 crédit chacune)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "views"
                ],
                "properties": {
                  "views": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    }
                  },
                  "instructions": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Une seule vue : sa consigne en anglais, à la place de celle par défaut"
                  },
                  "quote_only": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Vues en cours (cost_estimate)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subject"
                }
              }
            }
          },
          "400": {
            "description": "Vue absente du genre, plus de 6 images, instructions invalides"
          },
          "404": {
            "description": "Sujet inconnu"
          },
          "409": {
            "description": "subject_busy, subject_not_generated, view_exists ou start_image_missing"
          }
        }
      }
    },
    "/subjects/{id}/views/{key}/redo": {
      "post": {
        "summary": "(À venir, 0.9.1) Refaire une vue d'un sujet généré, ou son image de départ (start) tant qu'aucune vue n'en part (1 crédit)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Clé de la vue, ou start"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "instructions": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "quote_only": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "En cours (cost_estimate)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subject"
                }
              }
            }
          },
          "400": {
            "description": "Clé inconnue du genre (invalid_view) ou instructions invalides"
          },
          "404": {
            "description": "Sujet inconnu, ou vue jamais demandée (unknown_view)"
          },
          "409": {
            "description": "subject_busy, subject_not_generated, views_exist, start_image_imported ou start_image_missing"
          }
        }
      }
    },
    "/motions": {
      "post": {
        "summary": "Déposer une vidéo de mouvement (Multi-Ref Ultra, multiref.motion)",
        "description": "Rend un lien de dépôt signé (15 min) : PUT upload_url avec exactement les headers rendus, puis multiref.motion.motion_id dans POST /videos dans les 2 heures. Une vidéo de 4 à 30 s, 20 Mo au plus, ramenée en 480p au rendu ; une seule utilisation. 503 motion_not_open tant que le mouvement n'est pas ouvert.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "size"
                ],
                "properties": {
                  "size": {
                    "type": "integer",
                    "description": "Octets du fichier, 20 Mo au plus"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "video/mp4"
                    ],
                    "default": "video/mp4"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lien de dépôt",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "motion_id": {
                      "type": "string"
                    },
                    "upload_url": {
                      "type": "string"
                    },
                    "headers": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "expires_in": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Vidéo trop lourde (20 Mo au plus)"
          },
          "503": {
            "description": "Mouvement pas encore ouvert (motion_not_open) ou stockage indisponible"
          }
        }
      }
    },
    "/extensions": {
      "post": {
        "summary": "Déposer l'original d'un plan Ultra à prolonger (plus de 24 h)",
        "description": "Rend un lien de dépôt signé (15 min) : PUT upload_url avec exactement les headers rendus, le fichier ORIGINAL du plan tel que livré par GET /videos/{jobId}/file (MP4, 60 Mo au plus ; un fichier réencodé ou remonté perd la signature du moteur, que celui-ci exige pour un visage réaliste). Puis POST /videos en Ultra avec extend_upload_id dans les 2 heures ; une seule utilisation. Un plan Ultra de moins de 24 h se prolonge sans dépôt, par extend_job_id. Vidéo de 4 à 30 s, 1080p au plus, 24 à 60 i/s, rapport 0,4 à 2,5 (limites du moteur, vérifiées avant l'envoi : 400 input_video_out_of_limits, rien n'est débité).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "size"
                ],
                "properties": {
                  "size": {
                    "type": "integer",
                    "description": "Octets du fichier, 60 Mo au plus"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "video/mp4"
                    ],
                    "default": "video/mp4"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lien de dépôt",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "upload_id": {
                      "type": "string"
                    },
                    "upload_url": {
                      "type": "string"
                    },
                    "headers": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "expires_in": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Fichier trop lourd (extension_too_heavy, 60 Mo au plus)"
          },
          "503": {
            "description": "Prolongation pas encore ouverte (extension_not_open) ou stockage indisponible"
          }
        }
      }
    },
    "/subjects/{id}/images/{n}/file": {
      "get": {
        "summary": "Une image du sujet, telle que déposée",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "n",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 6
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Image",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Image absente"
          }
        }
      }
    },
    "/faceswap": {
      "post": {
        "summary": "Face swap (personnage entier) : 6 crédits par seconde de passage",
        "description": "La sortie est la vidéo du passage au format de travail (environ 1 million de pixels, proportions à 5 % près). Suivi, fichier et annulation : ceux d'une vidéo (/videos/{jobId}).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FaceSwapIn"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rendu accepté : jobId, seed, cost, et le passage préparé (width, height, length, fps, seconds, audio, voice, views, heads_only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Soumission"
                }
              }
            }
          },
          "400": {
            "description": "Paramètre invalide",
            "passage trop court ou contenu interdit": null
          },
          "401": {
            "$ref": "#/components/responses/NonAutorise"
          },
          "402": {
            "description": "Crédits insuffisants"
          },
          "403": {
            "$ref": "#/components/responses/Refuse"
          },
          "404": {
            "description": "Personnage inconnu"
          },
          "413": {
            "description": "Vidéo de plus de 20 Mo, voix de plus de 2 Mo, ou passage trop lourd une fois préparé"
          },
          "415": {
            "description": "Vidéo ou voix illisible"
          },
          "429": {
            "description": "Plafond de la clé, ou server_busy : deux vidéos déjà en préparation, réessayer après Retry-After (20 s)"
          },
          "503": {
            "description": "Face swap fermé (face_swap_unavailable), sans prix (face_swap_price_missing) ou personnages indisponibles (storage_unavailable)"
          }
        }
      }
    },
    "/videos/quote": {
      "post": {
        "summary": "Prix d'une vidéo sans la soumettre (rien n'est créé ni réservé)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VideoIn"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Devis",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Devis"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAutorise"
          }
        }
      }
    },
    "/pricing": {
      "get": {
        "summary": "Grille de prix du jour (sans clé)",
        "security": [],
        "responses": {
          "200": {
            "description": "Grille calculée depuis la configuration du proxy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "unit": {
                      "type": "string"
                    },
                    "image": {
                      "type": "object",
                      "properties": {
                        "cost": {
                          "type": "number"
                        }
                      }
                    },
                    "video": {
                      "type": "object",
                      "properties": {
                        "flat": {
                          "type": "number"
                        },
                        "format_multiplier": {
                          "type": "object"
                        },
                        "audio_multiplier": {
                          "type": "number"
                        },
                        "examples": {
                          "type": "object"
                        }
                      }
                    },
                    "turbo": {
                      "type": "object",
                      "properties": {
                        "seconds_per_credit": {
                          "type": "number"
                        },
                        "examples": {
                          "type": "object"
                        }
                      }
                    },
                    "ultra": {
                      "type": "object",
                      "description": "Prix fixes (p720, hd, msr_hd) et Ultra à la seconde : per_second (crédits la seconde par résolution 480p, 720p, 1080p), per_second_open (l'Ultra se rend ainsi pour toutes les clés), multiref_per_second_open (le Multi-Ref Ultra aussi), motion_per_second (crédits par seconde de vidéo de mouvement), extend_per_second (crédits par seconde de vidéo prolongée, par résolution ; null tant que la prolongation n'est pas ouverte)",
                      "properties": {
                        "open": {
                          "type": "boolean"
                        },
                        "p720": {
                          "type": "number",
                          "nullable": true
                        },
                        "hd": {
                          "type": "number",
                          "nullable": true
                        },
                        "msr_hd": {
                          "type": "number",
                          "nullable": true
                        },
                        "per_second": {
                          "type": "object",
                          "nullable": true
                        },
                        "per_second_open": {
                          "type": "boolean"
                        },
                        "multiref_per_second_open": {
                          "type": "boolean"
                        },
                        "motion_per_second": {
                          "type": "number",
                          "nullable": true
                        },
                        "extend_per_second": {
                          "type": "object",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "summary": "Cette spécification, avec les prix du jour (sans clé)",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.0 en JSON"
          }
        }
      }
    },
    "/openapi.yaml": {
      "get": {
        "summary": "Cette spécification en YAML, avec les prix du jour (sans clé)",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.0 en YAML"
          }
        }
      }
    }
  }
}
