@giveitsmaller/contracts 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/accepted-options/accepted-options.json +75 -1
  2. package/accepted-options/image-output-routes.json +13 -13
  3. package/availability/availability.json +518 -149
  4. package/dist/openapi/models/AccountLimitEntry.d.ts +1 -1
  5. package/dist/openapi/models/AccountLimitEntry.js +1 -1
  6. package/dist/openapi/models/AccountLimits.d.ts +1 -1
  7. package/dist/openapi/models/AccountLimits.js +1 -1
  8. package/dist/openapi/models/AccountLimitsLimits.d.ts +1 -1
  9. package/dist/openapi/models/AccountLimitsLimits.js +1 -1
  10. package/dist/openapi/models/AccountLimitsSuccessEnvelope.d.ts +1 -1
  11. package/dist/openapi/models/AccountLimitsSuccessEnvelope.js +1 -1
  12. package/dist/openapi/models/AudioWatermarkDecodeRequest.d.ts +1 -1
  13. package/dist/openapi/models/AudioWatermarkDecodeRequest.js +1 -1
  14. package/dist/openapi/models/AudioWatermarkDecodeResponse.d.ts +1 -1
  15. package/dist/openapi/models/AudioWatermarkDecodeResponse.js +1 -1
  16. package/dist/openapi/models/AuthErrorResponse.d.ts +1 -1
  17. package/dist/openapi/models/AuthErrorResponse.js +1 -1
  18. package/dist/openapi/models/AuthErrorType.d.ts +1 -1
  19. package/dist/openapi/models/AuthErrorType.js +1 -1
  20. package/dist/openapi/models/AuthRejectionEnvelope.d.ts +1 -1
  21. package/dist/openapi/models/AuthRejectionEnvelope.js +1 -1
  22. package/dist/openapi/models/AvailabilityValue.d.ts +1 -1
  23. package/dist/openapi/models/AvailabilityValue.js +1 -1
  24. package/dist/openapi/models/BalanceExhaustedResponse.d.ts +1 -1
  25. package/dist/openapi/models/BalanceExhaustedResponse.js +1 -1
  26. package/dist/openapi/models/BalanceExhaustedResponseAllOfLinks.d.ts +1 -1
  27. package/dist/openapi/models/BalanceExhaustedResponseAllOfLinks.js +1 -1
  28. package/dist/openapi/models/CallbackEventType.d.ts +1 -1
  29. package/dist/openapi/models/CallbackEventType.js +1 -1
  30. package/dist/openapi/models/ChangePasswordRequest.d.ts +1 -1
  31. package/dist/openapi/models/ChangePasswordRequest.js +1 -1
  32. package/dist/openapi/models/CompositionPlan.d.ts +1 -1
  33. package/dist/openapi/models/CompositionPlan.js +1 -1
  34. package/dist/openapi/models/CompositionPlanJob.d.ts +1 -1
  35. package/dist/openapi/models/CompositionPlanJob.js +1 -1
  36. package/dist/openapi/models/CompositionPlanOperation.d.ts +1 -1
  37. package/dist/openapi/models/CompositionPlanOperation.js +1 -1
  38. package/dist/openapi/models/ConfirmEmailChange200Response.d.ts +1 -1
  39. package/dist/openapi/models/ConfirmEmailChange200Response.js +1 -1
  40. package/dist/openapi/models/ConfirmEmailChange200ResponseData.d.ts +1 -1
  41. package/dist/openapi/models/ConfirmEmailChange200ResponseData.js +1 -1
  42. package/dist/openapi/models/ConfirmEmailChangeRequest.d.ts +1 -1
  43. package/dist/openapi/models/ConfirmEmailChangeRequest.js +1 -1
  44. package/dist/openapi/models/ConnectionSource.d.ts +1 -1
  45. package/dist/openapi/models/ConnectionSource.js +1 -1
  46. package/dist/openapi/models/ContactRequest.d.ts +1 -1
  47. package/dist/openapi/models/ContactRequest.js +1 -1
  48. package/dist/openapi/models/ContactSubject.d.ts +1 -1
  49. package/dist/openapi/models/ContactSubject.js +1 -1
  50. package/dist/openapi/models/ContactValidationErrorResponse.d.ts +1 -1
  51. package/dist/openapi/models/ContactValidationErrorResponse.js +1 -1
  52. package/dist/openapi/models/CreateApiKey201Response.d.ts +1 -1
  53. package/dist/openapi/models/CreateApiKey201Response.js +1 -1
  54. package/dist/openapi/models/CreateApiKey201ResponseData.d.ts +1 -1
  55. package/dist/openapi/models/CreateApiKey201ResponseData.js +1 -1
  56. package/dist/openapi/models/CreateApiKeyRequest.d.ts +1 -1
  57. package/dist/openapi/models/CreateApiKeyRequest.js +1 -1
  58. package/dist/openapi/models/CreateExternalImport403Response.d.ts +1 -1
  59. package/dist/openapi/models/CreateExternalImport403Response.js +1 -1
  60. package/dist/openapi/models/CreateExternalImport422Response.d.ts +1 -1
  61. package/dist/openapi/models/CreateExternalImport422Response.js +1 -1
  62. package/dist/openapi/models/CreateWorkflow422Response.d.ts +1 -1
  63. package/dist/openapi/models/CreateWorkflow422Response.js +1 -1
  64. package/dist/openapi/models/CreditTransaction.d.ts +1 -1
  65. package/dist/openapi/models/CreditTransaction.js +1 -1
  66. package/dist/openapi/models/CreditTransactionSourceBucket.d.ts +1 -1
  67. package/dist/openapi/models/CreditTransactionSourceBucket.js +1 -1
  68. package/dist/openapi/models/CreditsBalanceResponse.d.ts +1 -1
  69. package/dist/openapi/models/CreditsBalanceResponse.js +1 -1
  70. package/dist/openapi/models/CreditsBalanceSuccessEnvelope.d.ts +1 -1
  71. package/dist/openapi/models/CreditsBalanceSuccessEnvelope.js +1 -1
  72. package/dist/openapi/models/CreditsUsageResponse.d.ts +1 -1
  73. package/dist/openapi/models/CreditsUsageResponse.js +1 -1
  74. package/dist/openapi/models/CreditsUsageSuccessEnvelope.d.ts +1 -1
  75. package/dist/openapi/models/CreditsUsageSuccessEnvelope.js +1 -1
  76. package/dist/openapi/models/Delivery.d.ts +1 -1
  77. package/dist/openapi/models/Delivery.js +1 -1
  78. package/dist/openapi/models/DeliveryOutputRef.d.ts +1 -1
  79. package/dist/openapi/models/DeliveryOutputRef.js +1 -1
  80. package/dist/openapi/models/DeliveryPlan.d.ts +1 -1
  81. package/dist/openapi/models/DeliveryPlan.js +1 -1
  82. package/dist/openapi/models/DeliveryPlanOutput.d.ts +1 -1
  83. package/dist/openapi/models/DeliveryPlanOutput.js +1 -1
  84. package/dist/openapi/models/DeliveryPlanReason.d.ts +1 -1
  85. package/dist/openapi/models/DeliveryPlanReason.js +1 -1
  86. package/dist/openapi/models/DeliverySelection.d.ts +1 -1
  87. package/dist/openapi/models/DeliverySelection.js +1 -1
  88. package/dist/openapi/models/DownloadBundle.d.ts +1 -1
  89. package/dist/openapi/models/DownloadBundle.js +1 -1
  90. package/dist/openapi/models/EmptySuccessEnvelope.d.ts +1 -1
  91. package/dist/openapi/models/EmptySuccessEnvelope.js +1 -1
  92. package/dist/openapi/models/EndpointProjection.d.ts +1 -1
  93. package/dist/openapi/models/EndpointProjection.js +1 -1
  94. package/dist/openapi/models/ErrorEnvelope.d.ts +1 -1
  95. package/dist/openapi/models/ErrorEnvelope.js +1 -1
  96. package/dist/openapi/models/EstimateQuality.d.ts +1 -1
  97. package/dist/openapi/models/EstimateQuality.js +1 -1
  98. package/dist/openapi/models/EstimateRange.d.ts +1 -1
  99. package/dist/openapi/models/EstimateRange.js +1 -1
  100. package/dist/openapi/models/ExternalDestination.d.ts +1 -1
  101. package/dist/openapi/models/ExternalDestination.js +1 -1
  102. package/dist/openapi/models/ExternalImportCreatedResponse.d.ts +1 -1
  103. package/dist/openapi/models/ExternalImportCreatedResponse.js +1 -1
  104. package/dist/openapi/models/ExternalImportCreatedSuccessEnvelope.d.ts +1 -1
  105. package/dist/openapi/models/ExternalImportCreatedSuccessEnvelope.js +1 -1
  106. package/dist/openapi/models/ExternalImportRequest.d.ts +1 -1
  107. package/dist/openapi/models/ExternalImportRequest.js +1 -1
  108. package/dist/openapi/models/ExternalImportToken.d.ts +1 -1
  109. package/dist/openapi/models/ExternalImportToken.js +1 -1
  110. package/dist/openapi/models/ExternalSource.d.ts +1 -1
  111. package/dist/openapi/models/ExternalSource.js +1 -1
  112. package/dist/openapi/models/FeatureNotAvailableResponse.d.ts +1 -1
  113. package/dist/openapi/models/FeatureNotAvailableResponse.js +1 -1
  114. package/dist/openapi/models/FeatureTierRestrictedResponse.d.ts +1 -1
  115. package/dist/openapi/models/FeatureTierRestrictedResponse.js +1 -1
  116. package/dist/openapi/models/FeatureViolation.d.ts +1 -1
  117. package/dist/openapi/models/FeatureViolation.js +1 -1
  118. package/dist/openapi/models/ForgotPasswordRequest.d.ts +1 -1
  119. package/dist/openapi/models/ForgotPasswordRequest.js +1 -1
  120. package/dist/openapi/models/ImageEncodeCapabilities.d.ts +1 -1
  121. package/dist/openapi/models/ImageEncodeCapabilities.js +1 -1
  122. package/dist/openapi/models/JobDefinition.d.ts +1 -1
  123. package/dist/openapi/models/JobDefinition.js +1 -1
  124. package/dist/openapi/models/JobDownload.d.ts +1 -1
  125. package/dist/openapi/models/JobDownload.js +1 -1
  126. package/dist/openapi/models/JobInputV2.d.ts +1 -1
  127. package/dist/openapi/models/JobInputV2.js +1 -1
  128. package/dist/openapi/models/JobMediaClass.d.ts +1 -1
  129. package/dist/openapi/models/JobMediaClass.js +1 -1
  130. package/dist/openapi/models/JobOutputSource.d.ts +1 -1
  131. package/dist/openapi/models/JobOutputSource.js +1 -1
  132. package/dist/openapi/models/JobResponse.d.ts +1 -1
  133. package/dist/openapi/models/JobResponse.js +1 -1
  134. package/dist/openapi/models/JobStatus.d.ts +1 -1
  135. package/dist/openapi/models/JobStatus.js +1 -1
  136. package/dist/openapi/models/JobType.d.ts +1 -1
  137. package/dist/openapi/models/JobType.js +1 -1
  138. package/dist/openapi/models/LivenessResponse.d.ts +1 -1
  139. package/dist/openapi/models/LivenessResponse.js +1 -1
  140. package/dist/openapi/models/LoginUser200Response.d.ts +1 -1
  141. package/dist/openapi/models/LoginUser200Response.js +1 -1
  142. package/dist/openapi/models/LoginUser200ResponseData.d.ts +1 -1
  143. package/dist/openapi/models/LoginUser200ResponseData.js +1 -1
  144. package/dist/openapi/models/LoginUser200ResponseDataUser.d.ts +1 -1
  145. package/dist/openapi/models/LoginUser200ResponseDataUser.js +1 -1
  146. package/dist/openapi/models/LoginUserRequest.d.ts +1 -1
  147. package/dist/openapi/models/LoginUserRequest.js +1 -1
  148. package/dist/openapi/models/MetadataResponse.d.ts +1 -1
  149. package/dist/openapi/models/MetadataResponse.js +1 -1
  150. package/dist/openapi/models/MetadataResponseDimensions.d.ts +1 -1
  151. package/dist/openapi/models/MetadataResponseDimensions.js +1 -1
  152. package/dist/openapi/models/MetadataResponseExif.d.ts +1 -1
  153. package/dist/openapi/models/MetadataResponseExif.js +1 -1
  154. package/dist/openapi/models/MetadataResponseExifGps.d.ts +1 -1
  155. package/dist/openapi/models/MetadataResponseExifGps.js +1 -1
  156. package/dist/openapi/models/MetadataSuccessEnvelope.d.ts +1 -1
  157. package/dist/openapi/models/MetadataSuccessEnvelope.js +1 -1
  158. package/dist/openapi/models/MimeGroupSchema.d.ts +1 -1
  159. package/dist/openapi/models/MimeGroupSchema.js +1 -1
  160. package/dist/openapi/models/MultiInputSource.d.ts +1 -1
  161. package/dist/openapi/models/MultiInputSource.js +1 -1
  162. package/dist/openapi/models/MultipartCompleteRequest.d.ts +1 -1
  163. package/dist/openapi/models/MultipartCompleteRequest.js +1 -1
  164. package/dist/openapi/models/MultipartCompleteRequestPartsInner.d.ts +1 -1
  165. package/dist/openapi/models/MultipartCompleteRequestPartsInner.js +1 -1
  166. package/dist/openapi/models/MultipartCompleteResponse.d.ts +1 -1
  167. package/dist/openapi/models/MultipartCompleteResponse.js +1 -1
  168. package/dist/openapi/models/MultipartCompleteSuccessEnvelope.d.ts +1 -1
  169. package/dist/openapi/models/MultipartCompleteSuccessEnvelope.js +1 -1
  170. package/dist/openapi/models/MultipartInitiateRequestMetadataHint.d.ts +1 -1
  171. package/dist/openapi/models/MultipartInitiateRequestMetadataHint.js +1 -1
  172. package/dist/openapi/models/MultipartInitiateResponse.d.ts +1 -1
  173. package/dist/openapi/models/MultipartInitiateResponse.js +1 -1
  174. package/dist/openapi/models/MultipartInitiateSuccessEnvelope.d.ts +1 -1
  175. package/dist/openapi/models/MultipartInitiateSuccessEnvelope.js +1 -1
  176. package/dist/openapi/models/MultipartKeepaliveResponse.d.ts +1 -1
  177. package/dist/openapi/models/MultipartKeepaliveResponse.js +1 -1
  178. package/dist/openapi/models/MultipartKeepaliveSuccessEnvelope.d.ts +1 -1
  179. package/dist/openapi/models/MultipartKeepaliveSuccessEnvelope.js +1 -1
  180. package/dist/openapi/models/MultipartPartListing.d.ts +1 -1
  181. package/dist/openapi/models/MultipartPartListing.js +1 -1
  182. package/dist/openapi/models/MultipartPresignRequest.d.ts +1 -1
  183. package/dist/openapi/models/MultipartPresignRequest.js +1 -1
  184. package/dist/openapi/models/MultipartPresignResponse.d.ts +1 -1
  185. package/dist/openapi/models/MultipartPresignResponse.js +1 -1
  186. package/dist/openapi/models/MultipartPresignSuccessEnvelope.d.ts +1 -1
  187. package/dist/openapi/models/MultipartPresignSuccessEnvelope.js +1 -1
  188. package/dist/openapi/models/MultipartStatusResponse.d.ts +1 -1
  189. package/dist/openapi/models/MultipartStatusResponse.js +1 -1
  190. package/dist/openapi/models/MultipartStatusSuccessEnvelope.d.ts +1 -1
  191. package/dist/openapi/models/MultipartStatusSuccessEnvelope.js +1 -1
  192. package/dist/openapi/models/OperationDefinition.d.ts +1 -1
  193. package/dist/openapi/models/OperationDefinition.js +1 -1
  194. package/dist/openapi/models/OperationDownload.d.ts +1 -1
  195. package/dist/openapi/models/OperationDownload.js +1 -1
  196. package/dist/openapi/models/OperationInputModel.d.ts +1 -1
  197. package/dist/openapi/models/OperationInputModel.js +1 -1
  198. package/dist/openapi/models/OperationResponse.d.ts +1 -1
  199. package/dist/openapi/models/OperationResponse.js +1 -1
  200. package/dist/openapi/models/OperationResult.d.ts +1 -1
  201. package/dist/openapi/models/OperationResult.js +1 -1
  202. package/dist/openapi/models/OperationResultMetadata.d.ts +1 -1
  203. package/dist/openapi/models/OperationResultMetadata.js +1 -1
  204. package/dist/openapi/models/OperationResultMetrics.d.ts +1 -1
  205. package/dist/openapi/models/OperationResultMetrics.js +1 -1
  206. package/dist/openapi/models/OperationSchemaDefinition.d.ts +1 -1
  207. package/dist/openapi/models/OperationSchemaDefinition.js +1 -1
  208. package/dist/openapi/models/OperationStatus.d.ts +1 -1
  209. package/dist/openapi/models/OperationStatus.js +1 -1
  210. package/dist/openapi/models/OperationType.d.ts +3 -3
  211. package/dist/openapi/models/OperationType.js +3 -3
  212. package/dist/openapi/models/OperationsSchemaResponse.d.ts +1 -1
  213. package/dist/openapi/models/OperationsSchemaResponse.js +1 -1
  214. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeatures.d.ts +1 -1
  215. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeatures.js +1 -1
  216. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDelivery.d.ts +1 -1
  217. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDelivery.js +1 -1
  218. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliveryMode.d.ts +1 -1
  219. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliveryMode.js +1 -1
  220. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliverySelection.d.ts +1 -1
  221. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliverySelection.js +1 -1
  222. package/dist/openapi/models/OptionSchema.d.ts +1 -1
  223. package/dist/openapi/models/OptionSchema.js +1 -1
  224. package/dist/openapi/models/PerRoleCardinalityEntry.d.ts +1 -1
  225. package/dist/openapi/models/PerRoleCardinalityEntry.js +1 -1
  226. package/dist/openapi/models/PerValueAvailabilityEntry.d.ts +1 -1
  227. package/dist/openapi/models/PerValueAvailabilityEntry.js +1 -1
  228. package/dist/openapi/models/PresignedUrlPart.d.ts +1 -1
  229. package/dist/openapi/models/PresignedUrlPart.js +1 -1
  230. package/dist/openapi/models/ProbePendingResponse.d.ts +1 -1
  231. package/dist/openapi/models/ProbePendingResponse.js +1 -1
  232. package/dist/openapi/models/ProcessingClass.d.ts +1 -1
  233. package/dist/openapi/models/ProcessingClass.js +1 -1
  234. package/dist/openapi/models/ProcessingClassBandViolation.d.ts +1 -1
  235. package/dist/openapi/models/ProcessingClassBandViolation.js +1 -1
  236. package/dist/openapi/models/ProcessingClassConstraints.d.ts +1 -1
  237. package/dist/openapi/models/ProcessingClassConstraints.js +1 -1
  238. package/dist/openapi/models/ProcessingClassEntry.d.ts +1 -1
  239. package/dist/openapi/models/ProcessingClassEntry.js +1 -1
  240. package/dist/openapi/models/ProcessingClassExceedsBandResponse.d.ts +1 -1
  241. package/dist/openapi/models/ProcessingClassExceedsBandResponse.js +1 -1
  242. package/dist/openapi/models/ProcessingClassHint.d.ts +1 -1
  243. package/dist/openapi/models/ProcessingClassHint.js +1 -1
  244. package/dist/openapi/models/ProcessingClassReason.d.ts +1 -1
  245. package/dist/openapi/models/ProcessingClassReason.js +1 -1
  246. package/dist/openapi/models/ProcessingClassRejectReason.d.ts +1 -1
  247. package/dist/openapi/models/ProcessingClassRejectReason.js +1 -1
  248. package/dist/openapi/models/ProcessingPlan.d.ts +1 -1
  249. package/dist/openapi/models/ProcessingPlan.js +1 -1
  250. package/dist/openapi/models/ProcessingPlanJob.d.ts +1 -1
  251. package/dist/openapi/models/ProcessingPlanJob.js +1 -1
  252. package/dist/openapi/models/ReEncodeDecision.d.ts +1 -1
  253. package/dist/openapi/models/ReEncodeDecision.js +1 -1
  254. package/dist/openapi/models/ReadinessResponse.d.ts +1 -1
  255. package/dist/openapi/models/ReadinessResponse.js +1 -1
  256. package/dist/openapi/models/RegisterUser422Response.d.ts +1 -1
  257. package/dist/openapi/models/RegisterUser422Response.js +1 -1
  258. package/dist/openapi/models/RegisterUserRequest.d.ts +1 -1
  259. package/dist/openapi/models/RegisterUserRequest.js +1 -1
  260. package/dist/openapi/models/ResetPasswordRequest.d.ts +1 -1
  261. package/dist/openapi/models/ResetPasswordRequest.js +1 -1
  262. package/dist/openapi/models/ResponseEnvelope.d.ts +1 -1
  263. package/dist/openapi/models/ResponseEnvelope.js +1 -1
  264. package/dist/openapi/models/RetryResponse.d.ts +1 -1
  265. package/dist/openapi/models/RetryResponse.js +1 -1
  266. package/dist/openapi/models/RetrySuccessEnvelope.d.ts +1 -1
  267. package/dist/openapi/models/RetrySuccessEnvelope.js +1 -1
  268. package/dist/openapi/models/SseCompletionBase.d.ts +1 -1
  269. package/dist/openapi/models/SseCompletionBase.js +1 -1
  270. package/dist/openapi/models/SseEventType.d.ts +1 -1
  271. package/dist/openapi/models/SseEventType.js +1 -1
  272. package/dist/openapi/models/SseJobCompletedData.d.ts +1 -1
  273. package/dist/openapi/models/SseJobCompletedData.js +1 -1
  274. package/dist/openapi/models/SseJobFailedData.d.ts +1 -1
  275. package/dist/openapi/models/SseJobFailedData.js +1 -1
  276. package/dist/openapi/models/SseMultiOutputCompletion.d.ts +1 -1
  277. package/dist/openapi/models/SseMultiOutputCompletion.js +1 -1
  278. package/dist/openapi/models/SseMultiOutputCompletionMetrics.d.ts +1 -1
  279. package/dist/openapi/models/SseMultiOutputCompletionMetrics.js +1 -1
  280. package/dist/openapi/models/SseMultiOutputCompletionWithKind.d.ts +1 -1
  281. package/dist/openapi/models/SseMultiOutputCompletionWithKind.js +1 -1
  282. package/dist/openapi/models/SseMultiOutputResultEntry.d.ts +1 -1
  283. package/dist/openapi/models/SseMultiOutputResultEntry.js +1 -1
  284. package/dist/openapi/models/SseOperationCompletedData.d.ts +1 -1
  285. package/dist/openapi/models/SseOperationCompletedData.js +1 -1
  286. package/dist/openapi/models/SseOperationCompletionResult.d.ts +1 -1
  287. package/dist/openapi/models/SseOperationCompletionResult.js +1 -1
  288. package/dist/openapi/models/SseOperationFailedData.d.ts +1 -1
  289. package/dist/openapi/models/SseOperationFailedData.js +1 -1
  290. package/dist/openapi/models/SseOperationProgressData.d.ts +1 -1
  291. package/dist/openapi/models/SseOperationProgressData.js +1 -1
  292. package/dist/openapi/models/SseSingleOutputCompletion.d.ts +1 -1
  293. package/dist/openapi/models/SseSingleOutputCompletion.js +1 -1
  294. package/dist/openapi/models/SseWorkflowTerminalData.d.ts +1 -1
  295. package/dist/openapi/models/SseWorkflowTerminalData.js +1 -1
  296. package/dist/openapi/models/TierRestrictionKind.d.ts +1 -1
  297. package/dist/openapi/models/TierRestrictionKind.js +1 -1
  298. package/dist/openapi/models/TierRestrictionResponse.d.ts +1 -1
  299. package/dist/openapi/models/TierRestrictionResponse.js +1 -1
  300. package/dist/openapi/models/UpdateProfile200Response.d.ts +1 -1
  301. package/dist/openapi/models/UpdateProfile200Response.js +1 -1
  302. package/dist/openapi/models/UpdateProfile200ResponseData.d.ts +1 -1
  303. package/dist/openapi/models/UpdateProfile200ResponseData.js +1 -1
  304. package/dist/openapi/models/UpdateProfile422Response.d.ts +1 -1
  305. package/dist/openapi/models/UpdateProfile422Response.js +1 -1
  306. package/dist/openapi/models/UpdateProfileRequest.d.ts +1 -1
  307. package/dist/openapi/models/UpdateProfileRequest.js +1 -1
  308. package/dist/openapi/models/UploadConstraintsApplied.d.ts +1 -1
  309. package/dist/openapi/models/UploadConstraintsApplied.js +1 -1
  310. package/dist/openapi/models/UploadDurationExceedsTierResponse.d.ts +1 -1
  311. package/dist/openapi/models/UploadDurationExceedsTierResponse.js +1 -1
  312. package/dist/openapi/models/UploadFile403Response.d.ts +1 -1
  313. package/dist/openapi/models/UploadFile403Response.js +1 -1
  314. package/dist/openapi/models/UploadFile422Response.d.ts +1 -1
  315. package/dist/openapi/models/UploadFile422Response.js +1 -1
  316. package/dist/openapi/models/UploadProbeMediaMetadata.d.ts +1 -1
  317. package/dist/openapi/models/UploadProbeMediaMetadata.js +1 -1
  318. package/dist/openapi/models/UploadProbeProcessingClass.d.ts +1 -1
  319. package/dist/openapi/models/UploadProbeProcessingClass.js +1 -1
  320. package/dist/openapi/models/UploadProbeResponse.d.ts +1 -1
  321. package/dist/openapi/models/UploadProbeResponse.js +1 -1
  322. package/dist/openapi/models/UploadProbeStatus.d.ts +1 -1
  323. package/dist/openapi/models/UploadProbeStatus.js +1 -1
  324. package/dist/openapi/models/UploadProbeSuccessEnvelope.d.ts +1 -1
  325. package/dist/openapi/models/UploadProbeSuccessEnvelope.js +1 -1
  326. package/dist/openapi/models/UploadResponse.d.ts +1 -1
  327. package/dist/openapi/models/UploadResponse.js +1 -1
  328. package/dist/openapi/models/UploadSizeExceedsTierResponse.d.ts +1 -1
  329. package/dist/openapi/models/UploadSizeExceedsTierResponse.js +1 -1
  330. package/dist/openapi/models/UploadSource.d.ts +1 -1
  331. package/dist/openapi/models/UploadSource.js +1 -1
  332. package/dist/openapi/models/UploadSuccessEnvelope.d.ts +1 -1
  333. package/dist/openapi/models/UploadSuccessEnvelope.js +1 -1
  334. package/dist/openapi/models/UploadThresholds.d.ts +1 -1
  335. package/dist/openapi/models/UploadThresholds.js +1 -1
  336. package/dist/openapi/models/UserTier.d.ts +1 -1
  337. package/dist/openapi/models/UserTier.js +1 -1
  338. package/dist/openapi/models/ValidationErrorEnvelope.d.ts +1 -1
  339. package/dist/openapi/models/ValidationErrorEnvelope.js +1 -1
  340. package/dist/openapi/models/ValidationErrorEnvelopeDetailsInner.d.ts +1 -1
  341. package/dist/openapi/models/ValidationErrorEnvelopeDetailsInner.js +1 -1
  342. package/dist/openapi/models/VerifyEmailRequest.d.ts +1 -1
  343. package/dist/openapi/models/VerifyEmailRequest.js +1 -1
  344. package/dist/openapi/models/WarningType.d.ts +1 -1
  345. package/dist/openapi/models/WarningType.js +1 -1
  346. package/dist/openapi/models/WebhookOperationContext.d.ts +1 -1
  347. package/dist/openapi/models/WebhookOperationContext.js +1 -1
  348. package/dist/openapi/models/WebhookPayload.d.ts +1 -1
  349. package/dist/openapi/models/WebhookPayload.js +1 -1
  350. package/dist/openapi/models/WorkflowCancelBillingEffect.d.ts +1 -1
  351. package/dist/openapi/models/WorkflowCancelBillingEffect.js +1 -1
  352. package/dist/openapi/models/WorkflowCancelResponse.d.ts +1 -1
  353. package/dist/openapi/models/WorkflowCancelResponse.js +1 -1
  354. package/dist/openapi/models/WorkflowCancelSuccessEnvelope.d.ts +1 -1
  355. package/dist/openapi/models/WorkflowCancelSuccessEnvelope.js +1 -1
  356. package/dist/openapi/models/WorkflowCreateRequest.d.ts +1 -1
  357. package/dist/openapi/models/WorkflowCreateRequest.js +1 -1
  358. package/dist/openapi/models/WorkflowCreateResponse.d.ts +1 -1
  359. package/dist/openapi/models/WorkflowCreateResponse.js +1 -1
  360. package/dist/openapi/models/WorkflowCreateSuccessEnvelope.d.ts +1 -1
  361. package/dist/openapi/models/WorkflowCreateSuccessEnvelope.js +1 -1
  362. package/dist/openapi/models/WorkflowDownloadResponse.d.ts +1 -1
  363. package/dist/openapi/models/WorkflowDownloadResponse.js +1 -1
  364. package/dist/openapi/models/WorkflowDownloadSuccessEnvelope.d.ts +1 -1
  365. package/dist/openapi/models/WorkflowDownloadSuccessEnvelope.js +1 -1
  366. package/dist/openapi/models/WorkflowEdge.d.ts +1 -1
  367. package/dist/openapi/models/WorkflowEdge.js +1 -1
  368. package/dist/openapi/models/WorkflowExpiredResponse.d.ts +1 -1
  369. package/dist/openapi/models/WorkflowExpiredResponse.js +1 -1
  370. package/dist/openapi/models/WorkflowListResponse.d.ts +1 -1
  371. package/dist/openapi/models/WorkflowListResponse.js +1 -1
  372. package/dist/openapi/models/WorkflowListSuccessEnvelope.d.ts +1 -1
  373. package/dist/openapi/models/WorkflowListSuccessEnvelope.js +1 -1
  374. package/dist/openapi/models/WorkflowPauseRequiredAction.d.ts +1 -1
  375. package/dist/openapi/models/WorkflowPauseRequiredAction.js +1 -1
  376. package/dist/openapi/models/WorkflowPausedDetail.d.ts +1 -1
  377. package/dist/openapi/models/WorkflowPausedDetail.js +1 -1
  378. package/dist/openapi/models/WorkflowPausedDetailLinks.d.ts +1 -1
  379. package/dist/openapi/models/WorkflowPausedDetailLinks.js +1 -1
  380. package/dist/openapi/models/WorkflowProcessing.d.ts +1 -1
  381. package/dist/openapi/models/WorkflowProcessing.js +1 -1
  382. package/dist/openapi/models/WorkflowResumeResponse.d.ts +1 -1
  383. package/dist/openapi/models/WorkflowResumeResponse.js +1 -1
  384. package/dist/openapi/models/WorkflowResumeSuccessEnvelope.d.ts +1 -1
  385. package/dist/openapi/models/WorkflowResumeSuccessEnvelope.js +1 -1
  386. package/dist/openapi/models/WorkflowSource.d.ts +1 -1
  387. package/dist/openapi/models/WorkflowSource.js +1 -1
  388. package/dist/openapi/models/WorkflowStatus.d.ts +1 -1
  389. package/dist/openapi/models/WorkflowStatus.js +1 -1
  390. package/dist/openapi/models/WorkflowStatusResponse.d.ts +1 -1
  391. package/dist/openapi/models/WorkflowStatusResponse.js +1 -1
  392. package/dist/openapi/models/WorkflowStatusSuccessEnvelope.d.ts +1 -1
  393. package/dist/openapi/models/WorkflowStatusSuccessEnvelope.js +1 -1
  394. package/dist/openapi/models/WorkflowSummary.d.ts +1 -1
  395. package/dist/openapi/models/WorkflowSummary.js +1 -1
  396. package/dist/openapi/models/WorkflowSummaryJob.d.ts +1 -1
  397. package/dist/openapi/models/WorkflowSummaryJob.js +1 -1
  398. package/dist/openapi/models/WorkflowWarning.d.ts +1 -1
  399. package/dist/openapi/models/WorkflowWarning.js +1 -1
  400. package/dist/openapi/models/WorkflowWarningSeverity.d.ts +1 -1
  401. package/dist/openapi/models/WorkflowWarningSeverity.js +1 -1
  402. package/dist/openapi/runtime.d.ts +1 -1
  403. package/dist/openapi/runtime.js +1 -1
  404. package/dist/operations/audio_overlay.metadata.js +2 -2
  405. package/dist/operations/compress.metadata.js +0 -5
  406. package/dist/operations/convert.metadata.js +0 -2
  407. package/dist/operations/image_watermark.d.ts +36 -0
  408. package/dist/operations/image_watermark.js +24 -0
  409. package/dist/operations/image_watermark.metadata.js +47 -1
  410. package/dist/operations/split.metadata.js +1 -1
  411. package/dist/operations/text_watermark.d.ts +62 -0
  412. package/dist/operations/text_watermark.js +42 -0
  413. package/dist/operations/text_watermark.metadata.js +82 -0
  414. package/dist/operations/thumbnail.d.ts +19 -0
  415. package/dist/operations/thumbnail.js +12 -0
  416. package/dist/operations/thumbnail.metadata.js +29 -0
  417. package/dist/operations/video_watermark.metadata.js +1 -1
  418. package/openapi/api.yaml +3 -3
  419. package/operation-capabilities/operation-capabilities.json +1 -1
  420. package/operations/schemas/archive.yaml +1 -1
  421. package/operations/schemas/audio_overlay.yaml +9 -11
  422. package/operations/schemas/audio_to_video.yaml +7 -11
  423. package/operations/schemas/audio_watermark.yaml +1 -5
  424. package/operations/schemas/compress.yaml +322 -399
  425. package/operations/schemas/convert.yaml +64 -85
  426. package/operations/schemas/custom_luma.yaml +6 -11
  427. package/operations/schemas/image_watermark.yaml +166 -14
  428. package/operations/schemas/merge.yaml +5 -5
  429. package/operations/schemas/passthrough.yaml +4 -4
  430. package/operations/schemas/split.yaml +22 -31
  431. package/operations/schemas/text_watermark.yaml +262 -4
  432. package/operations/schemas/thumbnail.yaml +73 -3
  433. package/operations/schemas/video_text_watermark.yaml +9 -13
  434. package/operations/schemas/video_watermark.yaml +16 -21
  435. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "capabilities_version": 136,
2
+ "capabilities_version": 142,
3
3
  "endpoints": {
4
4
  "DELETE /api/auth/api-keys/{apiKeyId}": {
5
5
  "auth": "required",
@@ -267,7 +267,7 @@
267
267
  "options": {
268
268
  "folder_structure": {
269
269
  "default": "flat",
270
- "description": "flat = all files together at the top level (default); by_job = a separate subfolder for each source (planned \u2014 not yet honored by the worker)",
270
+ "description": "flat = all files together at the top level (default); by_job = a separate subfolder for each source (planned \u2014 not yet supported)",
271
271
  "per_value_availability": {
272
272
  "by_job": {
273
273
  "availability": "planned"
@@ -294,15 +294,15 @@
294
294
  "audio_overlay": {
295
295
  "availability": "planned",
296
296
  "default": false,
297
- "description": "Mix a secondary audio asset (overlay) over a primary audio or\nvideo base. Use cases: DJ tags, podcast intros/outros, station\nIDs, jingles, branded mid-rolls.\n\n**Industry naming.** This is \"audio overlay\" or \"audio branding\"\n\u2014 NOT \"audio watermark\". Audio watermarking is steganographic\n(imperceptible identifier embedded in audio for ownership /\nforensic tracking) and is owned by the separate `audio_watermark`\noperation (planned per ticket I20).\n\nMulti-input (Path B with `role: base` + `role: overlay` per\n`JobInputV2.role`). `min_inputs: 2 = max_inputs: 2` for V2.0;\nmulti-overlay support is advertised as a planned feature\n(`features.multi_overlay_stack`) but Lambda-side implementation\ndeferred.\n\nEach input is a `MultiInputSource` \u2014 imported externally,\nreferenced from a vault connection, or an upstream `job_output`\n(uploads are NOT referenced directly inside inputs[]). An uploaded\noverlay or base enters via a `passthrough` source job referenced by\n`{ type: job_output, from: <id> }` (per ticket 4som89Uh). The base\nis whatever single-asset source the workflow\nupstream has \u2014 for `mime_groups.audio`, the base is the primary\naudio track; for `mime_groups.video`, the overlay is mixed into\nthe video's audio track and the visual track passes through\nuntouched.\n\nPer ADR-0001 \u00a71.3 (Tension 1 \u2014 `planned` operations return\n`feature_not_available` 422 until Lambda support ships). Per\nADR-0004 \u00a7\"V2 JobDefinition\" + plan v5 \u00a7F1 round 6.\n",
297
+ "description": "Mix a secondary audio asset (overlay) over a primary audio or\nvideo base. Use cases: DJ tags, podcast intros/outros, station\nIDs, jingles, branded mid-rolls.\n\n**Industry naming.** This is \"audio overlay\" or \"audio branding\"\n\u2014 NOT \"audio watermark\". Audio watermarking is steganographic\n(imperceptible identifier embedded in audio for ownership /\nforensic tracking) and is owned by the separate `audio_watermark`\noperation (planned per ticket I20).\n\nMulti-input (Path B with `role: base` + `role: overlay` per\nthe input role). `min_inputs: 2 = max_inputs: 2` for V2.0;\nmulti-overlay support is advertised as a planned feature\n(`features.multi_overlay_stack`) but Backend implementation\ndeferred.\n\nEach input is imported externally,\nreferenced from a vault connection, or an upstream `job_output`\n(uploads are NOT referenced directly inside inputs[]). An uploaded\noverlay or base enters via a `passthrough` source job referenced by\n`{ type: job_output, from: <id> }` (per ticket 4som89Uh). The base\nis whatever single-asset source the workflow\nupstream has \u2014 for `mime_groups.audio`, the base is the primary\naudio track; for `mime_groups.video`, the overlay is mixed into\nthe video's audio track and the visual track passes through\nuntouched.\n\n`planned` \u2014 not yet supported.\n",
298
298
  "features": {
299
299
  "multi_overlay_stack": {
300
300
  "availability": "planned",
301
- "description": "Allow up to 8 overlay inputs per job (currently capped at 1\noverlay). Per-overlay placement via\n`JobInputV2.per_input_options`. Lambda-side support not yet\nconfirmed; tagged `planned` per ADR-0001 \u00a71.4. Mirrors the\nsame-named feature on `image_watermark`.\n"
301
+ "description": "Allow up to 8 overlay inputs per job (currently capped at 1\noverlay). Per-overlay placement via\nper-input placement options. Backend support not yet\nconfirmed; tagged `planned`. Mirrors the same-named feature on\n`image_watermark`.\n"
302
302
  },
303
303
  "sync_to_silence": {
304
304
  "availability": "planned",
305
- "description": "Anchor overlay placement to detected silence regions in the\nbase track (e.g. drop a podcast jingle into the natural\nbreath after the host says \"back in a sec\"). Server-side\nsilence detection runs at workflow-create time; the actual\noverlay start position is reported on\n`OperationMetrics.overlay_anchor_ms`. Gates two extra\noptions: `silence_threshold_db` and `min_silence_ms`. Lambda\nsupport not yet shipped.\n"
305
+ "description": "Anchor overlay placement to detected silence regions in the\nbase track (e.g. drop a podcast jingle into the natural\nbreath after the host says \"back in a sec\"). Server-side\nsilence detection runs at workflow-create time; the actual\noverlay start position is reported on\n`OperationMetrics.overlay_anchor_ms`. Gates two extra\noptions: `silence_threshold_db` and `min_silence_ms`. backend\nsupport not yet shipped.\n"
306
306
  }
307
307
  },
308
308
  "input_model": "multi",
@@ -569,7 +569,7 @@
569
569
  },
570
570
  "no_audio_track_behaviour": {
571
571
  "default": "reject",
572
- "description": "Controls behaviour when the base video has no audio\ntrack:\n- `reject`: workflow-create returns 422 (default \u2014 surfaces\n the problem at the API edge rather than producing a\n surprising silent-base output).\n- `create_silent`: synthesise a silent base track at the\n video's framerate and mix the overlay into that.\n- `replace`: same as `create_silent` but signals intent\n (\"the overlay IS the audio track\"), letting Lambda\n skip the base-track synthesis when the overlay covers\n the full video duration.\n",
572
+ "description": "Controls behaviour when the base video has no audio\ntrack:\n- `reject`: workflow-create returns 422 (default \u2014 surfaces\n the problem at the API edge rather than producing a\n surprising silent-base output).\n- `create_silent`: synthesise a silent base track at the\n video's framerate and mix the overlay into that.\n- `replace`: same as `create_silent` but signals intent\n (\"the overlay IS the audio track\"), letting backend\n skip the base-track synthesis when the overlay covers\n the full video duration.\n",
573
573
  "type": "enum",
574
574
  "values": [
575
575
  "reject",
@@ -642,7 +642,7 @@
642
642
  "audio_to_video": {
643
643
  "availability": "beta",
644
644
  "default": false,
645
- "description": "Produce a video from an audio input plus an OPTIONAL still image\noverlay. Use cases: podcast \u2192 YouTube uploads, audio-only tracks\nthat need a video container for platforms requiring it, branded\naudio releases with a static cover image.\n\n**Role asymmetry.** First role-based operation on the contract\nwith an OPTIONAL role \u2014 REQUIRED `base` (audio source) +\nOPTIONAL `overlay` (still image, 0..1). When `overlay` is\nomitted, the video uses a solid `background_color`. The\nmachine-readable cardinality lives in `per_role_cardinality`\non this schema (per ADR-0015). API consumption site:\n`OperationInputRoleValidator` (rule table) consumed by\n`SchemaValidationService.php` in `compression_api`.\n\n**Input vs output asymmetry.** The single `mime_groups.audio`\nblock keys the INPUT MIME family (the `base` role input is\naudio). The OUTPUT is always video \u2014 `output_format` controls\nthe container; `mime_groups` is NOT the output type. Mirrors\nthe `audio_overlay.video` group pattern (input-keyed).\n\nRe-encode required (audio \u2192 video composition always re-encodes).\nLower framerate + FFmpeg `-tune stillimage` keeps file size\ntiny when the overlay is a still image (the common case).\n\nPer ticket [`SlluxMBN`](https://trello.com/c/SlluxMBN) +\nADR-0015. Activated to `availability: beta` (Wave A, ticket\n[`c3uthIP4`](https://trello.com/c/c3uthIP4)): the cross-repo\nLambda backend is live (API accepts + publishes), so this is\nshape-stable + opt-in for callers and MUST NOT return\n`feature_not_available` \u2014 it may still change with notice while beta.\n",
645
+ "description": "Produce a video from an audio input plus an OPTIONAL still image\noverlay. Use cases: podcast \u2192 YouTube uploads, audio-only tracks\nthat need a video container for platforms requiring it, branded\naudio releases with a static cover image.\n\n**Role asymmetry.** First role-based operation on the contract\nwith an OPTIONAL role \u2014 REQUIRED `base` (audio source) +\nOPTIONAL `overlay` (still image, 0..1). When `overlay` is\nomitted, the video uses a solid `background_color`. The\nmachine-readable cardinality lives in `per_role_cardinality`\non this schema.\n\n**Input vs output asymmetry.** The single `mime_groups.audio`\nblock keys the INPUT MIME family (the `base` role input is\naudio). The OUTPUT is always video \u2014 `output_format` controls\nthe container; `mime_groups` is NOT the output type. Mirrors\nthe `audio_overlay.video` group pattern (input-keyed).\n\nRe-encode required (audio \u2192 video composition always re-encodes).\nA lower framerate plus still-image tuning keeps file size tiny\nwhen the overlay is a still image (the common case).\n\nActivated to `availability: beta`: the backend is live (API\naccepts + publishes), so this is shape-stable + opt-in for callers\nand MUST NOT return `feature_not_available` \u2014 it may still change\nwith notice while beta.\n",
646
646
  "input_model": "multi",
647
647
  "max_inputs": 2,
648
648
  "mime_groups": {
@@ -718,7 +718,7 @@
718
718
  "audio_watermark": {
719
719
  "availability": "planned",
720
720
  "default": false,
721
- "description": "Embed a steganographic forensic watermark into an audio asset (or\na video's audio track). The watermark is psychoacoustically masked\nto be inaudible and engineered to survive lossy compression,\ntranscoding, broadcast chains, and analog-hole capture (Cinavia /\nResemble PerTh territory).\n\n**Industry naming.** This is *audio watermarking* (steganographic;\nused for ownership, forensic tracking, content provenance) \u2014 NOT\n*audio overlay* (audible mixing of secondary audio for branding).\nAudio overlay is owned by the separate `audio_overlay` operation\n(per ticket I19).\n\nSingle-input. Watermark embedding takes the base asset and writes\nout an indistinguishable-to-the-ear copy with the encoded payload.\n\n**Decode endpoint.** A paired `POST /api/audio-watermark/decode`\nendpoint extracts the embedded watermark from a previously-marked\nasset, returning `{watermark_id, payload?, confidence, method,\ndetected_at}`. Decode is **own-watermarks-only** (rate-limited\n+ audited; the decoder will refuse to extract from media the\ncaller did not mark themselves) \u2014 pin per spike S11 acceptance.\n\nPer ADR-0001 \u00a71.3 (Tension 1 \u2014 `planned` operations return\n`feature_not_available` 422 until Lambda support ships).\nPer plan v5 \u00a7F1 round 6 (codex narrowed methods enum from 5 to 3,\ndropping `spread_spectrum` / `echo_hiding` / `phase_coding` \u2014\ntextbook families, not product choices).\n",
721
+ "description": "Embed a steganographic forensic watermark into an audio asset (or\na video's audio track). The watermark is psychoacoustically masked\nto be inaudible and engineered to survive lossy compression,\ntranscoding, broadcast chains, and analog-hole capture (Cinavia /\nResemble PerTh territory).\n\n**Industry naming.** This is *audio watermarking* (steganographic;\nused for ownership, forensic tracking, content provenance) \u2014 NOT\n*audio overlay* (audible mixing of secondary audio for branding).\nAudio overlay is owned by the separate `audio_overlay` operation\n(per ticket I19).\n\nSingle-input. Watermark embedding takes the base asset and writes\nout an indistinguishable-to-the-ear copy with the encoded payload.\n\n**Decode endpoint.** A paired `POST /api/audio-watermark/decode`\nendpoint extracts the embedded watermark from a previously-marked\nasset, returning `{watermark_id, payload?, confidence, method,\ndetected_at}`. Decode is **own-watermarks-only** (rate-limited\n+ audited; the decoder will refuse to extract from media the\ncaller did not mark themselves) \u2014 pin per spike S11 acceptance.\n\n`planned` \u2014 not yet supported.\n",
722
722
  "input_model": "single",
723
723
  "mime_groups": {
724
724
  "audio": {
@@ -882,7 +882,7 @@
882
882
  "options": {
883
883
  "bitrate": {
884
884
  "default": 128,
885
- "description": "Output bitrate in kbps (CBR/ABR). Applies to lossy OUTPUTS. Only a caller-EXPLICIT bitrate against a lossless output is rejected as invalid_options (flac/wav have no bitrate target); the `default: 128` is never the basis for a rejection (ADR-0020 D1, carried into ADR-0023 \u2014 a default a caller did not set is the server's to resolve, and is dropped for a lossless target). The output is the input format unless `output_format` re-targets it: with `output_format: original` the lossy/lossless distinction follows the input MIME (mpeg/aac/ogg/mp4 = lossy \u2192 bitrate applies; flac/wav input \u2192 an explicit bitrate is rejected); with an explicit `output_format`, it follows that target (flac/wav target \u2192 an explicit bitrate is rejected). Not expressed as `depends_on` \u2014 the lossy-output condition is an output-value \u00d7 input-MIME cross-axis the option-to-option `depends_on` cannot model without dropping `original` and breaking same-format callers. The other knobs (channels/sample_rate/normalize) apply to all formats.\n",
885
+ "description": "Output bitrate in kbps (CBR/ABR). Applies to lossy OUTPUTS. Only a caller-EXPLICIT bitrate against a lossless output is rejected as invalid_options (flac/wav have no bitrate target); the `default: 128` is never the basis for a rejection (a default a caller did not set is the server's to resolve, and is dropped for a lossless target). The output is the input format unless `output_format` re-targets it: with `output_format: original` the lossy/lossless distinction follows the input MIME (mpeg/aac/ogg/mp4 = lossy \u2192 bitrate applies; flac/wav input \u2192 an explicit bitrate is rejected); with an explicit `output_format`, it follows that target (flac/wav target \u2192 an explicit bitrate is rejected). Not expressed as `depends_on` \u2014 the lossy-output condition is an output-value \u00d7 input-MIME cross-axis the option-to-option `depends_on` cannot model without dropping `original` and breaking same-format callers. The other knobs (channels/sample_rate/normalize) apply to all formats.\n",
886
886
  "type": "enum",
887
887
  "value_type": "integer",
888
888
  "values": [
@@ -930,12 +930,12 @@
930
930
  ]
931
931
  },
932
932
  "trim_end": {
933
- "description": "Trim from end in seconds (absent = to the end). Removes this many seconds from the end; with `trim_start` the kept clip is [`trim_start`, input_duration \u2212 `trim_end`]. STABLE \u2014 see `trim_start`.\n",
933
+ "description": "Trim from end in seconds (absent = to the end). Removes this many seconds from the end; with `trim_start` the kept clip is [`trim_start`, input_duration \u2212 `trim_end`]. See `trim_start`.\n",
934
934
  "min": 0,
935
935
  "type": "float"
936
936
  },
937
937
  "trim_start": {
938
- "description": "Trim from beginning in seconds (absent = from the start). STABLE (qZI5EK9j) \u2014 the compress-audio worker cuts the clip before encoding (input-side ffmpeg `-ss`/`-t`; lambdas PR #259), SAME FROM-END semantic as compress.video / convert / merge: the kept clip is [`trim_start`, input_duration \u2212 `trim_end`].\n",
938
+ "description": "Trim from beginning in seconds (absent = from the start). The clip is cut before encoding, with the same from-end semantic as compress.video / convert / merge: the kept clip is [`trim_start`, input_duration \u2212 `trim_end`].\n",
939
939
  "min": 0,
940
940
  "type": "float"
941
941
  }
@@ -949,20 +949,20 @@
949
949
  "font_subsetting": {
950
950
  "availability": "planned",
951
951
  "default": true,
952
- "description": "Subset embedded fonts to only include glyphs used in the document. Significant size reduction for books with large font files. `planned` \u2014 not yet read by the worker.",
952
+ "description": "Subset embedded fonts to only include glyphs used in the document. Significant size reduction for books with large font files. `planned` \u2014 not yet supported.",
953
953
  "type": "boolean"
954
954
  },
955
955
  "image_quality": {
956
956
  "availability": "planned",
957
957
  "default": 80,
958
- "description": "Recompress embedded images at this quality level. `planned` \u2014 not yet read by the worker (use `quality`).",
958
+ "description": "Recompress embedded images at this quality level. `planned` \u2014 not yet supported (use `quality`).",
959
959
  "max": 100,
960
960
  "min": 1,
961
961
  "type": "integer"
962
962
  },
963
963
  "quality": {
964
964
  "default": 50,
965
- "description": "Compression quality (1 = smallest, 100 = best). The only worker-honored document option today.",
965
+ "description": "Compression quality (1 = smallest, 100 = best). The only document compression option supported today.",
966
966
  "max": 100,
967
967
  "min": 1,
968
968
  "type": "integer"
@@ -970,7 +970,7 @@
970
970
  "strip_unused_css": {
971
971
  "availability": "planned",
972
972
  "default": false,
973
- "description": "Remove CSS rules not referenced by any content. Risk: may affect rendering in some readers. `planned` \u2014 not yet read by the worker.",
973
+ "description": "Remove CSS rules not referenced by any content. Risk: may affect rendering in some readers. `planned` \u2014 not yet supported.",
974
974
  "type": "boolean"
975
975
  }
976
976
  }
@@ -985,14 +985,14 @@
985
985
  "image_quality": {
986
986
  "availability": "planned",
987
987
  "default": 80,
988
- "description": "Recompress embedded images at this quality level. `planned` \u2014 not yet read by the worker (use `quality`).",
988
+ "description": "Recompress embedded images at this quality level. `planned` \u2014 not yet supported (use `quality`).",
989
989
  "max": 100,
990
990
  "min": 1,
991
991
  "type": "integer"
992
992
  },
993
993
  "quality": {
994
994
  "default": 50,
995
- "description": "Compression quality (1 = smallest, 100 = best). The only worker-honored document option today.",
995
+ "description": "Compression quality (1 = smallest, 100 = best). The only document compression option supported today.",
996
996
  "max": 100,
997
997
  "min": 1,
998
998
  "type": "integer"
@@ -1000,13 +1000,13 @@
1000
1000
  "strip_metadata": {
1001
1001
  "availability": "planned",
1002
1002
  "default": true,
1003
- "description": "Remove document metadata (author, revision history, comments). `planned` \u2014 not yet read by the worker.",
1003
+ "description": "Remove document metadata (author, revision history, comments). `planned` \u2014 not yet supported.",
1004
1004
  "type": "boolean"
1005
1005
  },
1006
1006
  "strip_unused_styles": {
1007
1007
  "availability": "planned",
1008
1008
  "default": false,
1009
- "description": "Remove style definitions not referenced in document content. `planned` \u2014 not yet read by the worker.",
1009
+ "description": "Remove style definitions not referenced in document content. `planned` \u2014 not yet supported.",
1010
1010
  "type": "boolean"
1011
1011
  }
1012
1012
  }
@@ -1021,14 +1021,14 @@
1021
1021
  "image_quality": {
1022
1022
  "availability": "planned",
1023
1023
  "default": 80,
1024
- "description": "Recompress embedded images at this quality level (1 = smallest, 100 = best). `planned` \u2014 not yet read by the worker (use `quality`).",
1024
+ "description": "Recompress embedded images at this quality level (1 = smallest, 100 = best). `planned` \u2014 not yet supported (use `quality`).",
1025
1025
  "max": 100,
1026
1026
  "min": 1,
1027
1027
  "type": "integer"
1028
1028
  },
1029
1029
  "quality": {
1030
1030
  "default": 50,
1031
- "description": "Compression quality (1 = smallest, 100 = best). The only worker-honored document option today.",
1031
+ "description": "Compression quality (1 = smallest, 100 = best). The only document compression option supported today.",
1032
1032
  "max": 100,
1033
1033
  "min": 1,
1034
1034
  "type": "integer"
@@ -1036,19 +1036,19 @@
1036
1036
  "strip_hidden_data": {
1037
1037
  "availability": "planned",
1038
1038
  "default": true,
1039
- "description": "Remove revisions, comments, personal info, hidden rows/columns, and speaker notes. `planned` \u2014 not yet read by the worker.",
1039
+ "description": "Remove revisions, comments, personal info, hidden rows/columns, and speaker notes. `planned` \u2014 not yet supported.",
1040
1040
  "type": "boolean"
1041
1041
  },
1042
1042
  "strip_macros": {
1043
1043
  "availability": "planned",
1044
1044
  "default": true,
1045
- "description": "Remove VBA macros and ActiveX controls. `planned` \u2014 not yet read by the worker.",
1045
+ "description": "Remove VBA macros and ActiveX controls. `planned` \u2014 not yet supported.",
1046
1046
  "type": "boolean"
1047
1047
  },
1048
1048
  "strip_unused_fonts": {
1049
1049
  "availability": "planned",
1050
1050
  "default": false,
1051
- "description": "Remove embedded fonts not referenced in the document. Risk: may affect rendering on systems without the font installed. `planned` \u2014 not yet read by the worker.",
1051
+ "description": "Remove embedded fonts not referenced in the document. Risk: may affect rendering on systems without the font installed. `planned` \u2014 not yet supported.",
1052
1052
  "type": "boolean"
1053
1053
  }
1054
1054
  }
@@ -1061,7 +1061,7 @@
1061
1061
  "colorspace": {
1062
1062
  "availability": "planned",
1063
1063
  "default": "unchanged",
1064
- "description": "Output color space. `planned` \u2014 not yet read by the worker (use `grayscale` for grayscale conversion).",
1064
+ "description": "Output color space. `planned` \u2014 not yet supported (use `grayscale` for grayscale conversion).",
1065
1065
  "type": "enum",
1066
1066
  "values": [
1067
1067
  "unchanged",
@@ -1073,27 +1073,27 @@
1073
1073
  "flatten_forms": {
1074
1074
  "availability": "planned",
1075
1075
  "default": false,
1076
- "description": "Flatten interactive form fields into static content. `planned` \u2014 not yet read by the worker.",
1076
+ "description": "Flatten interactive form fields into static content. `planned` \u2014 not yet supported.",
1077
1077
  "type": "boolean"
1078
1078
  },
1079
1079
  "grayscale": {
1080
1080
  "default": false,
1081
- "description": "Convert all images to grayscale (smaller output; discards colour). Worker-honored.",
1081
+ "description": "Convert all images to grayscale (smaller output; discards colour).",
1082
1082
  "type": "boolean"
1083
1083
  },
1084
1084
  "image_dpi": {
1085
- "description": "Downsample images above this DPI (72\u2013600 inclusive). Omit to keep the worker's DPI analysis. Out-of-range is rejected as invalid_options.",
1085
+ "description": "Downsample images above this DPI (72\u2013600 inclusive). Omit to use automatic DPI analysis. Out-of-range is rejected.",
1086
1086
  "max": 600,
1087
1087
  "min": 72,
1088
1088
  "type": "integer"
1089
1089
  },
1090
1090
  "pages": {
1091
1091
  "availability": "planned",
1092
- "description": "Page selection (e.g. '1-5,8,10-12'). Omit to keep all pages. `planned` \u2014 not yet read by the worker.",
1092
+ "description": "Page selection (e.g. '1-5,8,10-12'). Omit to keep all pages. `planned` \u2014 not yet supported.",
1093
1093
  "type": "string"
1094
1094
  },
1095
1095
  "profile": {
1096
- "description": "Ghostscript optimization preset, pinned directly (overrides the quality\u2192preset mapping). screen = lowest-res / smallest, ebook = mid, printer = 300dpi, prepress = print-production. Optional \u2014 omit to let `quality` drive the preset.",
1096
+ "description": "Compression preset, pinned directly (overrides the quality\u2192preset mapping). screen = lowest-res / smallest, ebook = mid, printer = 300dpi, prepress = print-production. Optional \u2014 omit to let `quality` drive the preset.",
1097
1097
  "type": "enum",
1098
1098
  "values": [
1099
1099
  "screen",
@@ -1104,7 +1104,7 @@
1104
1104
  },
1105
1105
  "quality": {
1106
1106
  "default": 50,
1107
- "description": "Compression quality (1 = smallest, 100 = best). Maps to a Ghostscript preset. Overridden by `profile` when that is set.",
1107
+ "description": "Compression quality (1 = smallest, 100 = best). Maps to a compression preset. Overridden by `profile` when that is set.",
1108
1108
  "max": 100,
1109
1109
  "min": 1,
1110
1110
  "type": "integer"
@@ -1119,9 +1119,8 @@
1119
1119
  ],
1120
1120
  "options": {
1121
1121
  "auto_orient": {
1122
- "availability": "planned",
1123
1122
  "default": true,
1124
- "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. `planned` \u2014 a restored control (was removed): the worker must apply the EXIF-orientation transform before encode on both Output routes. Default `true` (a behaviour change, proven planned-first). No-op for inputs without an orientation tag.",
1123
+ "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. Default `true`; no-op for inputs without an orientation tag. On an already-rotated image the pixels are re-encoded (near-lossless), which can drop other (non-orientation) metadata even with `metadata: keep`.",
1125
1124
  "honored_on": [
1126
1125
  "same_format",
1127
1126
  "format_change"
@@ -1131,7 +1130,7 @@
1131
1130
  "color_profile": {
1132
1131
  "availability": "planned",
1133
1132
  "default": "keep",
1134
- "description": "ICC colour-profile handling \u2014 its OWN control, DECOUPLED from `metadata` (stripping metadata must NOT silently shift colours; that coupling is the correctness bug this fixes). `keep` (default) = preserve the embedded profile; `srgb` = convert to sRGB (also handles CMYK\u2192sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 the worker does not yet honour it (today the default strips ICC during metadata-strip; the fix preserves it). Applies on BOTH Output routes (optimiser preserves; transcoder preserves/converts). Worker-proven flip.",
1133
+ "description": "Colour-profile handling \u2014 a dedicated control, separate from `metadata` so that stripping metadata never silently shifts colours. `keep` (default) = preserve the embedded colour profile; `srgb` = convert to sRGB (also handles CMYK \u2192 sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 not yet supported.",
1135
1134
  "honored_on": [
1136
1135
  "same_format",
1137
1136
  "format_change"
@@ -1150,7 +1149,7 @@
1150
1149
  "logic": "or",
1151
1150
  "width": "set"
1152
1151
  },
1153
- "description": "Resize mode. `max` (default) = fit within bounds, preserving aspect ratio and NEVER upscaling \u2014 it caps at the source dimensions. This IS the \"no-enlarge\" behaviour (there is no separate `no_enlarge` flag \u2014 the worker has no such key). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past source). Applies only when width or height is set, on whichever Output route runs.",
1152
+ "description": "Resize mode. `max` (default) = fit within the given bounds, preserving aspect ratio and never upscaling (caps at the source dimensions \u2014 this is the \"no-enlarge\" behaviour). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past the source). Applies only when width or height is set.",
1154
1153
  "honored_on": [
1155
1154
  "same_format",
1156
1155
  "format_change"
@@ -1163,7 +1162,7 @@
1163
1162
  ]
1164
1163
  },
1165
1164
  "height": {
1166
- "description": "Resize-inside-Output: target height in pixels (1-16384; width*height <= max_output_pixels). Resize replaces the image (one output). Optional \u2014 see `width`.",
1165
+ "description": "Target height in pixels (1-16384; width \u00d7 height within the group's max output area). Resizes the image (one output). Optional \u2014 see `width`.",
1167
1166
  "honored_on": [
1168
1167
  "same_format",
1169
1168
  "format_change"
@@ -1177,11 +1176,16 @@
1177
1176
  "depends_on": {
1178
1177
  "metadata": "strip"
1179
1178
  },
1180
- "description": "Selective per-category metadata keep (optimiser/same_format route): preserve ONLY these categories and strip the rest \u2014 refines the strip path (`metadata: strip`). Valid categories: `copyright`, `gps` (location), `date` (capture time); combinable. `planned` \u2014 the worker does not yet do selective keep (use `metadata: keep` to preserve everything), so the API returns feature_not_available until lambdas proves it.",
1179
+ "description": "Selectively keep only certain metadata categories and strip the rest \u2014 refines `metadata: strip`. Allowed categories: `copyright`, `gps` (location), `date` (capture time); combinable. `planned` \u2014 selective keep is not yet supported (use `metadata: keep` to preserve everything).",
1181
1180
  "honored_on": [
1182
1181
  "same_format"
1183
1182
  ],
1184
1183
  "items": {
1184
+ "enum": [
1185
+ "copyright",
1186
+ "gps",
1187
+ "date"
1188
+ ],
1185
1189
  "type": "string"
1186
1190
  },
1187
1191
  "type": "array",
@@ -1189,7 +1193,7 @@
1189
1193
  },
1190
1194
  "metadata": {
1191
1195
  "default": "strip",
1192
- "description": "Metadata handling (optimiser/same_format route). `strip` = remove all EXIF/IPTC/XMP (default). `keep` = preserve EXIF/ICC/XMP \u2014 STABLE (worker-proven, libcaesium; lambdas PR #260, staging round-trip + unit test). `all` is a DEPRECATED alias of `strip` (the pre-2026-06-23 token; `all` reading as \"strip all\" was counterintuitive) \u2014 still accepted, emits Deprecation/Sunset, migrate to `strip`. The API lowers `strip`\u2192`all` at the worker boundary. Any other value is rejected as invalid_options.",
1196
+ "description": "Metadata handling. `strip` = remove all EXIF/IPTC/XMP (default). `keep` = preserve EXIF, colour profile, and XMP. `all` is a DEPRECATED alias of `strip` (kept for older callers; emits Deprecation/Sunset headers \u2014 migrate to `strip`). Any other value is rejected.",
1193
1197
  "honored_on": [
1194
1198
  "same_format"
1195
1199
  ],
@@ -1207,7 +1211,7 @@
1207
1211
  },
1208
1212
  "output_format": {
1209
1213
  "default": "original",
1210
- "description": "Output format. original = keep input format (default); webp = recompress to WebP \u2014 the one live compress+format target (proven on staging). Concrete format changes (jpeg/png/avif/gif/tiff) are the convert operation's job, not compress. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are SELECTION STRATEGIES, not concrete targets (convert cannot express \"pick the smallest\"), so they live here and stay `planned` until the API try-all-return-smallest logic + worker proof land. See FORMAT.md.",
1214
+ "description": "Output format. original = keep input format (default); webp = recompress to WebP. Concrete format changes (jpeg/png/avif/gif/tiff) are handled by the convert operation. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are selection strategies, not concrete targets, and stay `planned` until supported. See FORMAT.md.",
1211
1215
  "per_value_availability": {
1212
1216
  "auto": {
1213
1217
  "availability": "planned"
@@ -1226,7 +1230,7 @@
1226
1230
  },
1227
1231
  "quality": {
1228
1232
  "default": 80,
1229
- "description": "Compression quality (1 = smallest file, 100 = best quality). For animated GIF the worker honours this but real-world size reduction is typically small.",
1233
+ "description": "Compression quality (1 = smallest file, 100 = best quality). For animated GIF this is honoured but real-world size reduction is typically small.",
1230
1234
  "honored_on": [
1231
1235
  "same_format"
1232
1236
  ],
@@ -1235,7 +1239,7 @@
1235
1239
  "type": "integer"
1236
1240
  },
1237
1241
  "width": {
1238
- "description": "Resize-inside-Output: target width in pixels (1-16384; width*height <= max_output_pixels). Resize the image as part of Output (replace semantics, ONE transformed output) using the proven convert resize engine (Lanczos; the API routes Output resize through the convert worker, BOTH routes \u2014 the compress optimiser has no resize) \u2014 it is NOT the derivative `thumbnail` op. Optional; omit to keep the source dimensions.",
1242
+ "description": "Target width in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Resizes the image as part of the operation (one transformed output, replacing the source). Optional; omit to keep the source dimensions.",
1239
1243
  "honored_on": [
1240
1244
  "same_format",
1241
1245
  "format_change"
@@ -1254,9 +1258,8 @@
1254
1258
  ],
1255
1259
  "options": {
1256
1260
  "auto_orient": {
1257
- "availability": "planned",
1258
1261
  "default": true,
1259
- "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. `planned` \u2014 a restored control (was removed): the worker must apply the EXIF-orientation transform before encode on both Output routes. Default `true` (a behaviour change, proven planned-first). No-op for inputs without an orientation tag.",
1262
+ "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. Default `true`; no-op for inputs without an orientation tag. On an already-rotated image the pixels are re-encoded (near-lossless), which can drop other (non-orientation) metadata even with `metadata: keep`.",
1260
1263
  "honored_on": [
1261
1264
  "same_format",
1262
1265
  "format_change"
@@ -1265,7 +1268,7 @@
1265
1268
  },
1266
1269
  "avif_speed": {
1267
1270
  "default": 4,
1268
- "description": "AVIF encode speed (1-10, ravif). Lower = smaller file, slower encode; higher = faster, larger. Default 4. Optimiser (same_format) route.",
1271
+ "description": "AVIF encode speed (1-10). Lower = smaller file, slower encode; higher = faster, larger. Default 4.",
1269
1272
  "honored_on": [
1270
1273
  "same_format"
1271
1274
  ],
@@ -1276,7 +1279,7 @@
1276
1279
  "color_profile": {
1277
1280
  "availability": "planned",
1278
1281
  "default": "keep",
1279
- "description": "ICC colour-profile handling \u2014 its OWN control, DECOUPLED from `metadata` (stripping metadata must NOT silently shift colours; that coupling is the correctness bug this fixes). `keep` (default) = preserve the embedded profile; `srgb` = convert to sRGB (also handles CMYK\u2192sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 the worker does not yet honour it (today the default strips ICC during metadata-strip; the fix preserves it). Applies on BOTH Output routes (optimiser preserves; transcoder preserves/converts). Worker-proven flip.",
1282
+ "description": "Colour-profile handling \u2014 a dedicated control, separate from `metadata` so that stripping metadata never silently shifts colours. `keep` (default) = preserve the embedded colour profile; `srgb` = convert to sRGB (also handles CMYK \u2192 sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 not yet supported.",
1280
1283
  "honored_on": [
1281
1284
  "same_format",
1282
1285
  "format_change"
@@ -1290,7 +1293,7 @@
1290
1293
  },
1291
1294
  "encoding_mode": {
1292
1295
  "default": "quality",
1293
- "description": "Compression mode (mirrors compress.video). `quality` = the `quality` slider drives the encode (default). `target_size` = hit a byte budget via the worker's encode-measure-binary-search loop \u2014 STABLE (worker-proven on staging, lambdas git-870a7c2: re-encode under target + best-effort + original-wins). AVIF encodes are slow, so the loop caps iterations (a 6-iteration cap, ~18s worst case). Optimiser (same_format) route.",
1296
+ "description": "Compression mode. `quality` = the `quality` slider drives the encode (default). `target_size` = automatically find the highest quality that fits a byte budget (set via `target_size_bytes`); if the budget can't be met even at the lowest quality, the closest result is returned, and the original is kept if re-encoding wouldn't be smaller. AVIF encoding is slower, so the search is bounded and may stop slightly above the budget.",
1294
1297
  "honored_on": [
1295
1298
  "same_format"
1296
1299
  ],
@@ -1307,7 +1310,7 @@
1307
1310
  "logic": "or",
1308
1311
  "width": "set"
1309
1312
  },
1310
- "description": "Resize mode. `max` (default) = fit within bounds, preserving aspect ratio and NEVER upscaling \u2014 it caps at the source dimensions. This IS the \"no-enlarge\" behaviour (there is no separate `no_enlarge` flag \u2014 the worker has no such key). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past source). Applies only when width or height is set, on whichever Output route runs.",
1313
+ "description": "Resize mode. `max` (default) = fit within the given bounds, preserving aspect ratio and never upscaling (caps at the source dimensions \u2014 this is the \"no-enlarge\" behaviour). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past the source). Applies only when width or height is set.",
1311
1314
  "honored_on": [
1312
1315
  "same_format",
1313
1316
  "format_change"
@@ -1320,7 +1323,7 @@
1320
1323
  ]
1321
1324
  },
1322
1325
  "height": {
1323
- "description": "Resize-inside-Output: target height in pixels (1-16384; width*height <= max_output_pixels). Resize replaces the image (one output). Optional \u2014 see `width`.",
1326
+ "description": "Target height in pixels (1-16384; width \u00d7 height within the group's max output area). Resizes the image (one output). Optional \u2014 see `width`.",
1324
1327
  "honored_on": [
1325
1328
  "same_format",
1326
1329
  "format_change"
@@ -1331,7 +1334,7 @@
1331
1334
  },
1332
1335
  "metadata": {
1333
1336
  "default": "strip",
1334
- "description": "Metadata handling. `strip` = remove all EXIF/IPTC/XMP (default, the only supported behaviour). `all` is a DEPRECATED alias of `strip` (migrate to `strip`; the API lowers it at the worker boundary). `keep` is NOT offered for AVIF \u2014 ravif re-encodes from pixels and cannot preserve metadata (the worker rejects `keep` as invalid_options).",
1337
+ "description": "Metadata handling. `strip` = remove all EXIF/IPTC/XMP (default, the only supported behaviour). `all` is a DEPRECATED alias of `strip` (migrate to `strip`). `keep` is NOT offered for AVIF \u2014 it is re-encoded from pixels and cannot preserve metadata.",
1335
1338
  "honored_on": [
1336
1339
  "same_format"
1337
1340
  ],
@@ -1348,7 +1351,7 @@
1348
1351
  },
1349
1352
  "output_format": {
1350
1353
  "default": "original",
1351
- "description": "Output format. original = keep input format (default); webp = recompress to WebP \u2014 the one live compress+format target (proven on staging). Concrete format changes (jpeg/png/avif/gif/tiff) are the convert operation's job, not compress. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are SELECTION STRATEGIES, not concrete targets (convert cannot express \"pick the smallest\"), so they live here and stay `planned` until the API try-all-return-smallest logic + worker proof land. See FORMAT.md.",
1354
+ "description": "Output format. original = keep input format (default); webp = recompress to WebP. Concrete format changes (jpeg/png/avif/gif/tiff) are handled by the convert operation. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are selection strategies, not concrete targets, and stay `planned` until supported. See FORMAT.md.",
1352
1355
  "per_value_availability": {
1353
1356
  "auto": {
1354
1357
  "availability": "planned"
@@ -1370,7 +1373,7 @@
1370
1373
  "depends_on": {
1371
1374
  "encoding_mode": "quality"
1372
1375
  },
1373
- "description": "Compression quality (1 = smallest file, 100 = best quality). Active in `quality` mode (default); in `target_size` mode the encode-measure loop owns quality, so this option is inactive.",
1376
+ "description": "Compression quality (1 = smallest file, 100 = best quality). Active in `quality` mode (default); in `target_size` mode the quality is chosen automatically to hit the size budget, so this option is inactive.",
1374
1377
  "honored_on": [
1375
1378
  "same_format"
1376
1379
  ],
@@ -1382,7 +1385,7 @@
1382
1385
  "depends_on": {
1383
1386
  "encoding_mode": "target_size"
1384
1387
  },
1385
- "description": "Target output size in bytes (min 1024 = 1 KiB). The image encode-measure loop binary-searches quality to land at or under the target; best-effort if the target is unreachable at min quality (the result reports the chosen quality + whether the target was met). STABLE (worker-proven, lambdas git-870a7c2). Optimiser (same_format) route.",
1388
+ "description": "Target output size in bytes (min 1024 = 1 KiB). The encoder searches for the highest quality that lands at or under this size; if the target can't be met even at the lowest quality, the closest result is returned (the result reports the chosen quality and whether the target was met). Requires `encoding_mode: target_size`.",
1386
1389
  "honored_on": [
1387
1390
  "same_format"
1388
1391
  ],
@@ -1390,7 +1393,7 @@
1390
1393
  "type": "integer"
1391
1394
  },
1392
1395
  "width": {
1393
- "description": "Resize-inside-Output: target width in pixels (1-16384; width*height <= max_output_pixels). Resize the image as part of Output (replace semantics, ONE transformed output) using the proven convert resize engine (Lanczos; the API routes Output resize through the convert worker, BOTH routes \u2014 the compress optimiser has no resize) \u2014 it is NOT the derivative `thumbnail` op. Optional; omit to keep the source dimensions.",
1396
+ "description": "Target width in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Resizes the image as part of the operation (one transformed output, replacing the source). Optional; omit to keep the source dimensions.",
1394
1397
  "honored_on": [
1395
1398
  "same_format",
1396
1399
  "format_change"
@@ -1408,9 +1411,8 @@
1408
1411
  ],
1409
1412
  "options": {
1410
1413
  "auto_orient": {
1411
- "availability": "planned",
1412
1414
  "default": true,
1413
- "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. `planned` \u2014 a restored control (was removed): the worker must apply the EXIF-orientation transform before encode on both Output routes. Default `true` (a behaviour change, proven planned-first). No-op for inputs without an orientation tag.",
1415
+ "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. Default `true`; no-op for inputs without an orientation tag. On an already-rotated image the pixels are re-encoded (near-lossless), which can drop other (non-orientation) metadata even with `metadata: keep`.",
1414
1416
  "honored_on": [
1415
1417
  "same_format",
1416
1418
  "format_change"
@@ -1418,7 +1420,7 @@
1418
1420
  "type": "boolean"
1419
1421
  },
1420
1422
  "chroma_subsampling": {
1421
- "description": "JPEG chroma subsampling \u2014 the colour-resolution/size trade-off. `420` (4:2:0) = smallest, chroma at quarter resolution (default-ish for web photos); `422` (4:2:2) = half-horizontal; `444` (4:4:4) = full chroma (largest, best for sharp colour edges / text). JPEG ONLY (declared on image_jpeg only \u2014 ravif hardcodes 4:4:4, libwebp lossy locks 4:2:0). String values (the worker strict-matches strings). STABLE \u2014 worker-proven on staging (lambdas PR #264, deployed git-f2bef85: output JPEG SOF luma sampling bytes verified 0x22/0x21/0x11 for 420/422/444). INCOMPATIBLE with `lossless` (any chroma value): lossless JPEG is a DCT-coefficient copy and cannot resample chroma \u2014 see the chroma_subsampling.lossless_conflict constraint.",
1423
+ "description": "JPEG chroma subsampling \u2014 the colour-resolution/size trade-off. `420` (4:2:0) = smallest, chroma at quarter resolution (typical for web photos); `422` (4:2:2) = half horizontal resolution; `444` (4:4:4) = full chroma (largest, best for sharp colour edges / text). JPEG only. INCOMPATIBLE with `lossless` (any chroma value): a lossless JPEG copies the existing coefficients and cannot resample chroma.",
1422
1424
  "honored_on": [
1423
1425
  "same_format"
1424
1426
  ],
@@ -1432,7 +1434,7 @@
1432
1434
  "color_profile": {
1433
1435
  "availability": "planned",
1434
1436
  "default": "keep",
1435
- "description": "ICC colour-profile handling \u2014 its OWN control, DECOUPLED from `metadata` (stripping metadata must NOT silently shift colours; that coupling is the correctness bug this fixes). `keep` (default) = preserve the embedded profile; `srgb` = convert to sRGB (also handles CMYK\u2192sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 the worker does not yet honour it (today the default strips ICC during metadata-strip; the fix preserves it). Applies on BOTH Output routes (optimiser preserves; transcoder preserves/converts). Worker-proven flip.",
1437
+ "description": "Colour-profile handling \u2014 a dedicated control, separate from `metadata` so that stripping metadata never silently shifts colours. `keep` (default) = preserve the embedded colour profile; `srgb` = convert to sRGB (also handles CMYK \u2192 sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 not yet supported.",
1436
1438
  "honored_on": [
1437
1439
  "same_format",
1438
1440
  "format_change"
@@ -1446,7 +1448,7 @@
1446
1448
  },
1447
1449
  "encoding_mode": {
1448
1450
  "default": "quality",
1449
- "description": "Compression mode (mirrors compress.video). `quality` = the `quality` slider drives the encode (default). `target_size` = hit a byte budget via the worker's encode-measure-binary-search loop \u2014 STABLE (worker-proven on staging, lambdas git-870a7c2: re-encode under target + best-effort below floor + original-wins, all proven). Optimiser (same_format) route.",
1451
+ "description": "Compression mode. `quality` = the `quality` slider drives the encode (default). `target_size` = automatically find the highest quality that fits a byte budget (set via `target_size_bytes`); if the budget can't be met even at the lowest quality, the closest result is returned, and the original is kept if re-encoding wouldn't be smaller.",
1450
1452
  "honored_on": [
1451
1453
  "same_format"
1452
1454
  ],
@@ -1463,7 +1465,7 @@
1463
1465
  "logic": "or",
1464
1466
  "width": "set"
1465
1467
  },
1466
- "description": "Resize mode. `max` (default) = fit within bounds, preserving aspect ratio and NEVER upscaling \u2014 it caps at the source dimensions. This IS the \"no-enlarge\" behaviour (there is no separate `no_enlarge` flag \u2014 the worker has no such key). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past source). Applies only when width or height is set, on whichever Output route runs.",
1468
+ "description": "Resize mode. `max` (default) = fit within the given bounds, preserving aspect ratio and never upscaling (caps at the source dimensions \u2014 this is the \"no-enlarge\" behaviour). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past the source). Applies only when width or height is set.",
1467
1469
  "honored_on": [
1468
1470
  "same_format",
1469
1471
  "format_change"
@@ -1476,7 +1478,7 @@
1476
1478
  ]
1477
1479
  },
1478
1480
  "height": {
1479
- "description": "Resize-inside-Output: target height in pixels (1-16384; width*height <= max_output_pixels). Resize replaces the image (one output). Optional \u2014 see `width`.",
1481
+ "description": "Target height in pixels (1-16384; width \u00d7 height within the group's max output area). Resizes the image (one output). Optional \u2014 see `width`.",
1480
1482
  "honored_on": [
1481
1483
  "same_format",
1482
1484
  "format_change"
@@ -1490,11 +1492,16 @@
1490
1492
  "depends_on": {
1491
1493
  "metadata": "strip"
1492
1494
  },
1493
- "description": "Selective per-category metadata keep (optimiser/same_format route): preserve ONLY these categories and strip the rest \u2014 refines the strip path (`metadata: strip`). Valid categories: `copyright`, `gps` (location), `date` (capture time); combinable. `planned` \u2014 the worker does not yet do selective keep (use `metadata: keep` to preserve everything), so the API returns feature_not_available until lambdas proves it.",
1495
+ "description": "Selectively keep only certain metadata categories and strip the rest \u2014 refines `metadata: strip`. Allowed categories: `copyright`, `gps` (location), `date` (capture time); combinable. `planned` \u2014 selective keep is not yet supported (use `metadata: keep` to preserve everything).",
1494
1496
  "honored_on": [
1495
1497
  "same_format"
1496
1498
  ],
1497
1499
  "items": {
1500
+ "enum": [
1501
+ "copyright",
1502
+ "gps",
1503
+ "date"
1504
+ ],
1498
1505
  "type": "string"
1499
1506
  },
1500
1507
  "type": "array",
@@ -1505,7 +1512,7 @@
1505
1512
  "depends_on": {
1506
1513
  "encoding_mode": "quality"
1507
1514
  },
1508
- "description": "Lossless JPEG re-optimisation (optimiser path): optimises the Huffman tables WITHOUT re-encoding pixels \u2014 bit-preserving on the pixel data, no quality loss (it cannot recover detail a lossy JPEG already lost). STABLE \u2014 the compress-image worker honours the `lossless` bool (un-parked 2026-06-23). Only in `quality` mode \u2014 incompatible with `target_size` (the byte-budget loop is inherently lossy). Optimiser (same_format) route only.",
1515
+ "description": "Lossless JPEG re-optimisation: re-packs the file WITHOUT re-encoding the pixels \u2014 no quality loss (it cannot recover detail a lossy JPEG already lost). Only in `quality` mode \u2014 incompatible with `target_size` (hitting a byte budget is inherently lossy).",
1509
1516
  "honored_on": [
1510
1517
  "same_format"
1511
1518
  ],
@@ -1513,7 +1520,7 @@
1513
1520
  },
1514
1521
  "metadata": {
1515
1522
  "default": "strip",
1516
- "description": "Metadata handling (optimiser/same_format route). `strip` = remove all EXIF/IPTC/XMP (default). `keep` = preserve EXIF/ICC/XMP \u2014 STABLE (worker-proven, libcaesium; lambdas PR #260, staging round-trip + unit test). `all` is a DEPRECATED alias of `strip` (the pre-2026-06-23 token; `all` reading as \"strip all\" was counterintuitive) \u2014 still accepted, emits Deprecation/Sunset, migrate to `strip`. The API lowers `strip`\u2192`all` at the worker boundary. Any other value is rejected as invalid_options.",
1523
+ "description": "Metadata handling. `strip` = remove all EXIF/IPTC/XMP (default). `keep` = preserve EXIF, colour profile, and XMP. `all` is a DEPRECATED alias of `strip` (kept for older callers; emits Deprecation/Sunset headers \u2014 migrate to `strip`). Any other value is rejected.",
1517
1524
  "honored_on": [
1518
1525
  "same_format"
1519
1526
  ],
@@ -1531,7 +1538,7 @@
1531
1538
  },
1532
1539
  "output_format": {
1533
1540
  "default": "original",
1534
- "description": "Output format. original = keep input format (default); webp = recompress to WebP \u2014 the one live compress+format target (proven on staging). Concrete format changes (jpeg/png/avif/gif/tiff) are the convert operation's job, not compress. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are SELECTION STRATEGIES, not concrete targets (convert cannot express \"pick the smallest\"), so they live here and stay `planned` until the API try-all-return-smallest logic + worker proof land. See FORMAT.md.",
1541
+ "description": "Output format. original = keep input format (default); webp = recompress to WebP. Concrete format changes (jpeg/png/avif/gif/tiff) are handled by the convert operation. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are selection strategies, not concrete targets, and stay `planned` until supported. See FORMAT.md.",
1535
1542
  "per_value_availability": {
1536
1543
  "auto": {
1537
1544
  "availability": "planned"
@@ -1550,7 +1557,7 @@
1550
1557
  },
1551
1558
  "progressive": {
1552
1559
  "default": true,
1553
- "description": "Enable progressive JPEG rendering (a low-quality preview renders first, then refines). JPEG only \u2014 the optimiser (same_format) route.",
1560
+ "description": "Enable progressive JPEG rendering (a low-quality preview renders first, then refines). JPEG only.",
1554
1561
  "honored_on": [
1555
1562
  "same_format"
1556
1563
  ],
@@ -1561,7 +1568,7 @@
1561
1568
  "depends_on": {
1562
1569
  "encoding_mode": "quality"
1563
1570
  },
1564
- "description": "Compression quality (1 = smallest file, 100 = best quality). Active in `quality` mode (default); in `target_size` mode the encode-measure loop owns quality, so this option is inactive.",
1571
+ "description": "Compression quality (1 = smallest file, 100 = best quality). Active in `quality` mode (default); in `target_size` mode the quality is chosen automatically to hit the size budget, so this option is inactive.",
1565
1572
  "honored_on": [
1566
1573
  "same_format"
1567
1574
  ],
@@ -1573,7 +1580,7 @@
1573
1580
  "depends_on": {
1574
1581
  "encoding_mode": "target_size"
1575
1582
  },
1576
- "description": "Target output size in bytes (min 1024 = 1 KiB). The image encode-measure loop binary-searches quality to land at or under the target; best-effort if the target is unreachable at min quality (the result reports the chosen quality + whether the target was met). STABLE (worker-proven, lambdas git-870a7c2). Optimiser (same_format) route.",
1583
+ "description": "Target output size in bytes (min 1024 = 1 KiB). The encoder searches for the highest quality that lands at or under this size; if the target can't be met even at the lowest quality, the closest result is returned (the result reports the chosen quality and whether the target was met). Requires `encoding_mode: target_size`.",
1577
1584
  "honored_on": [
1578
1585
  "same_format"
1579
1586
  ],
@@ -1581,7 +1588,7 @@
1581
1588
  "type": "integer"
1582
1589
  },
1583
1590
  "width": {
1584
- "description": "Resize-inside-Output: target width in pixels (1-16384; width*height <= max_output_pixels). Resize the image as part of Output (replace semantics, ONE transformed output) using the proven convert resize engine (Lanczos; the API routes Output resize through the convert worker, BOTH routes \u2014 the compress optimiser has no resize) \u2014 it is NOT the derivative `thumbnail` op. Optional; omit to keep the source dimensions.",
1591
+ "description": "Target width in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Resizes the image as part of the operation (one transformed output, replacing the source). Optional; omit to keep the source dimensions.",
1585
1592
  "honored_on": [
1586
1593
  "same_format",
1587
1594
  "format_change"
@@ -1599,9 +1606,8 @@
1599
1606
  ],
1600
1607
  "options": {
1601
1608
  "auto_orient": {
1602
- "availability": "planned",
1603
1609
  "default": true,
1604
- "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. `planned` \u2014 a restored control (was removed): the worker must apply the EXIF-orientation transform before encode on both Output routes. Default `true` (a behaviour change, proven planned-first). No-op for inputs without an orientation tag.",
1610
+ "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. Default `true`; no-op for inputs without an orientation tag. On an already-rotated image the pixels are re-encoded (near-lossless), which can drop other (non-orientation) metadata even with `metadata: keep`.",
1605
1611
  "honored_on": [
1606
1612
  "same_format",
1607
1613
  "format_change"
@@ -1611,7 +1617,7 @@
1611
1617
  "color_profile": {
1612
1618
  "availability": "planned",
1613
1619
  "default": "keep",
1614
- "description": "ICC colour-profile handling \u2014 its OWN control, DECOUPLED from `metadata` (stripping metadata must NOT silently shift colours; that coupling is the correctness bug this fixes). `keep` (default) = preserve the embedded profile; `srgb` = convert to sRGB (also handles CMYK\u2192sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 the worker does not yet honour it (today the default strips ICC during metadata-strip; the fix preserves it). Applies on BOTH Output routes (optimiser preserves; transcoder preserves/converts). Worker-proven flip.",
1620
+ "description": "Colour-profile handling \u2014 a dedicated control, separate from `metadata` so that stripping metadata never silently shifts colours. `keep` (default) = preserve the embedded colour profile; `srgb` = convert to sRGB (also handles CMYK \u2192 sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 not yet supported.",
1615
1621
  "honored_on": [
1616
1622
  "same_format",
1617
1623
  "format_change"
@@ -1630,7 +1636,7 @@
1630
1636
  "logic": "or",
1631
1637
  "width": "set"
1632
1638
  },
1633
- "description": "Resize mode. `max` (default) = fit within bounds, preserving aspect ratio and NEVER upscaling \u2014 it caps at the source dimensions. This IS the \"no-enlarge\" behaviour (there is no separate `no_enlarge` flag \u2014 the worker has no such key). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past source). Applies only when width or height is set, on whichever Output route runs.",
1639
+ "description": "Resize mode. `max` (default) = fit within the given bounds, preserving aspect ratio and never upscaling (caps at the source dimensions \u2014 this is the \"no-enlarge\" behaviour). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past the source). Applies only when width or height is set.",
1634
1640
  "honored_on": [
1635
1641
  "same_format",
1636
1642
  "format_change"
@@ -1643,7 +1649,7 @@
1643
1649
  ]
1644
1650
  },
1645
1651
  "height": {
1646
- "description": "Resize-inside-Output: target height in pixels (1-16384; width*height <= max_output_pixels). Resize replaces the image (one output). Optional \u2014 see `width`.",
1652
+ "description": "Target height in pixels (1-16384; width \u00d7 height within the group's max output area). Resizes the image (one output). Optional \u2014 see `width`.",
1647
1653
  "honored_on": [
1648
1654
  "same_format",
1649
1655
  "format_change"
@@ -1657,11 +1663,16 @@
1657
1663
  "depends_on": {
1658
1664
  "metadata": "strip"
1659
1665
  },
1660
- "description": "Selective per-category metadata keep (optimiser/same_format route): preserve ONLY these categories and strip the rest \u2014 refines the strip path (`metadata: strip`). Valid categories: `copyright`, `gps` (location), `date` (capture time); combinable. `planned` \u2014 the worker does not yet do selective keep (use `metadata: keep` to preserve everything), so the API returns feature_not_available until lambdas proves it.",
1666
+ "description": "Selectively keep only certain metadata categories and strip the rest \u2014 refines `metadata: strip`. Allowed categories: `copyright`, `gps` (location), `date` (capture time); combinable. `planned` \u2014 selective keep is not yet supported (use `metadata: keep` to preserve everything).",
1661
1667
  "honored_on": [
1662
1668
  "same_format"
1663
1669
  ],
1664
1670
  "items": {
1671
+ "enum": [
1672
+ "copyright",
1673
+ "gps",
1674
+ "date"
1675
+ ],
1665
1676
  "type": "string"
1666
1677
  },
1667
1678
  "type": "array",
@@ -1670,7 +1681,7 @@
1670
1681
  "lossy": {
1671
1682
  "availability": "planned",
1672
1683
  "default": false,
1673
- "description": "Lossy PNG palette quantization (pngquant/imagequant-class) \u2014 a large saving for flat-colour PNGs. `planned` AND LICENSING-GATED: do not stable-flip until the lambdas licensing spike confirms a permissive quantizer path (libimagequant/pngquant is GPL-or-commercial). Optimiser (same_format) route only.",
1684
+ "description": "Lossy PNG palette quantization \u2014 a large saving for flat-colour PNGs. `planned` \u2014 not yet supported.",
1674
1685
  "honored_on": [
1675
1686
  "same_format"
1676
1687
  ],
@@ -1678,7 +1689,7 @@
1678
1689
  },
1679
1690
  "metadata": {
1680
1691
  "default": "strip",
1681
- "description": "Metadata handling (optimiser/same_format route). `strip` = remove all EXIF/IPTC/XMP (default). `keep` = preserve EXIF/ICC/XMP \u2014 STABLE (worker-proven, libcaesium; lambdas PR #260, staging round-trip + unit test). `all` is a DEPRECATED alias of `strip` (the pre-2026-06-23 token; `all` reading as \"strip all\" was counterintuitive) \u2014 still accepted, emits Deprecation/Sunset, migrate to `strip`. The API lowers `strip`\u2192`all` at the worker boundary. Any other value is rejected as invalid_options.",
1692
+ "description": "Metadata handling. `strip` = remove all EXIF/IPTC/XMP (default). `keep` = preserve EXIF, colour profile, and XMP. `all` is a DEPRECATED alias of `strip` (kept for older callers; emits Deprecation/Sunset headers \u2014 migrate to `strip`). Any other value is rejected.",
1682
1693
  "honored_on": [
1683
1694
  "same_format"
1684
1695
  ],
@@ -1696,7 +1707,7 @@
1696
1707
  },
1697
1708
  "optimization_level": {
1698
1709
  "default": 3,
1699
- "description": "PNG optimization effort (0-6, oxipng). Higher = smaller file, slower encode. Default 3. Optimiser (same_format) route.",
1710
+ "description": "PNG optimization effort (0-6). Higher = smaller file, slower encode. Default 3.",
1700
1711
  "honored_on": [
1701
1712
  "same_format"
1702
1713
  ],
@@ -1706,7 +1717,7 @@
1706
1717
  },
1707
1718
  "output_format": {
1708
1719
  "default": "original",
1709
- "description": "Output format. original = keep input format (default); webp = recompress to WebP \u2014 the one live compress+format target (proven on staging). Concrete format changes (jpeg/png/avif/gif/tiff) are the convert operation's job, not compress. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are SELECTION STRATEGIES, not concrete targets (convert cannot express \"pick the smallest\"), so they live here and stay `planned` until the API try-all-return-smallest logic + worker proof land. See FORMAT.md.",
1720
+ "description": "Output format. original = keep input format (default); webp = recompress to WebP. Concrete format changes (jpeg/png/avif/gif/tiff) are handled by the convert operation. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are selection strategies, not concrete targets, and stay `planned` until supported. See FORMAT.md.",
1710
1721
  "per_value_availability": {
1711
1722
  "auto": {
1712
1723
  "availability": "planned"
@@ -1734,7 +1745,7 @@
1734
1745
  "type": "integer"
1735
1746
  },
1736
1747
  "width": {
1737
- "description": "Resize-inside-Output: target width in pixels (1-16384; width*height <= max_output_pixels). Resize the image as part of Output (replace semantics, ONE transformed output) using the proven convert resize engine (Lanczos; the API routes Output resize through the convert worker, BOTH routes \u2014 the compress optimiser has no resize) \u2014 it is NOT the derivative `thumbnail` op. Optional; omit to keep the source dimensions.",
1748
+ "description": "Target width in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Resizes the image as part of the operation (one transformed output, replacing the source). Optional; omit to keep the source dimensions.",
1738
1749
  "honored_on": [
1739
1750
  "same_format",
1740
1751
  "format_change"
@@ -1752,7 +1763,7 @@
1752
1763
  "options": {
1753
1764
  "metadata": {
1754
1765
  "default": "strip",
1755
- "description": "Metadata handling. `strip` = remove all metadata (default, the only supported behaviour). `all` is a DEPRECATED alias of `strip` (migrate to `strip`; the API lowers it at the worker boundary). `keep` is NOT offered for SVG \u2014 SVGO optimises the markup and cannot preserve a metadata block (the worker rejects `keep` as invalid_options).",
1766
+ "description": "Metadata handling. `strip` = remove all metadata (default, the only supported behaviour). `all` is a DEPRECATED alias of `strip` (migrate to `strip`). `keep` is NOT offered for SVG \u2014 optimising the markup cannot preserve a metadata block.",
1756
1767
  "honored_on": [
1757
1768
  "same_format"
1758
1769
  ],
@@ -1769,7 +1780,7 @@
1769
1780
  },
1770
1781
  "output_format": {
1771
1782
  "default": "original",
1772
- "description": "Output format. original = keep input format (default); webp = recompress to WebP. Concrete format changes are the convert operation's job. `auto` / `smallest` are PLANNED auto-format directives (server picks the best/smallest candidate format, reported via the result `mime_type`) \u2014 selection strategies, not concrete targets, planned until the API logic + worker proof land. See FORMAT.md.",
1783
+ "description": "Output format. original = keep input format (default); webp = recompress to WebP. Concrete format changes are handled by the convert operation. `auto` / `smallest` are PLANNED auto-format directives (the server picks the best/smallest candidate format, reported via the result `mime_type`) \u2014 selection strategies, not concrete targets, planned until supported. See FORMAT.md.",
1773
1784
  "per_value_availability": {
1774
1785
  "auto": {
1775
1786
  "availability": "planned"
@@ -1805,9 +1816,8 @@
1805
1816
  ],
1806
1817
  "options": {
1807
1818
  "auto_orient": {
1808
- "availability": "planned",
1809
1819
  "default": true,
1810
- "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. `planned` \u2014 a restored control (was removed): the worker must apply the EXIF-orientation transform before encode on both Output routes. Default `true` (a behaviour change, proven planned-first). No-op for inputs without an orientation tag.",
1820
+ "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. Default `true`; no-op for inputs without an orientation tag. On an already-rotated image the pixels are re-encoded (near-lossless), which can drop other (non-orientation) metadata even with `metadata: keep`.",
1811
1821
  "honored_on": [
1812
1822
  "same_format",
1813
1823
  "format_change"
@@ -1817,7 +1827,7 @@
1817
1827
  "color_profile": {
1818
1828
  "availability": "planned",
1819
1829
  "default": "keep",
1820
- "description": "ICC colour-profile handling \u2014 its OWN control, DECOUPLED from `metadata` (stripping metadata must NOT silently shift colours; that coupling is the correctness bug this fixes). `keep` (default) = preserve the embedded profile; `srgb` = convert to sRGB (also handles CMYK\u2192sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 the worker does not yet honour it (today the default strips ICC during metadata-strip; the fix preserves it). Applies on BOTH Output routes (optimiser preserves; transcoder preserves/converts). Worker-proven flip.",
1830
+ "description": "Colour-profile handling \u2014 a dedicated control, separate from `metadata` so that stripping metadata never silently shifts colours. `keep` (default) = preserve the embedded colour profile; `srgb` = convert to sRGB (also handles CMYK \u2192 sRGB); `strip` = drop the profile (smallest \u2014 only when the consumer manages colour itself). `planned` \u2014 not yet supported.",
1821
1831
  "honored_on": [
1822
1832
  "same_format",
1823
1833
  "format_change"
@@ -1831,7 +1841,7 @@
1831
1841
  },
1832
1842
  "encoding_mode": {
1833
1843
  "default": "quality",
1834
- "description": "Compression mode (mirrors compress.video). `quality` = the `quality` slider drives the encode (default). `target_size` = hit a byte budget via the worker's encode-measure-binary-search loop \u2014 STABLE (worker-proven on staging, lambdas git-870a7c2: re-encode under target + best-effort below floor + original-wins, all proven). Optimiser (same_format) route.",
1844
+ "description": "Compression mode. `quality` = the `quality` slider drives the encode (default). `target_size` = automatically find the highest quality that fits a byte budget (set via `target_size_bytes`); if the budget can't be met even at the lowest quality, the closest result is returned, and the original is kept if re-encoding wouldn't be smaller.",
1835
1845
  "honored_on": [
1836
1846
  "same_format"
1837
1847
  ],
@@ -1848,7 +1858,7 @@
1848
1858
  "logic": "or",
1849
1859
  "width": "set"
1850
1860
  },
1851
- "description": "Resize mode. `max` (default) = fit within bounds, preserving aspect ratio and NEVER upscaling \u2014 it caps at the source dimensions. This IS the \"no-enlarge\" behaviour (there is no separate `no_enlarge` flag \u2014 the worker has no such key). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past source). Applies only when width or height is set, on whichever Output route runs.",
1861
+ "description": "Resize mode. `max` (default) = fit within the given bounds, preserving aspect ratio and never upscaling (caps at the source dimensions \u2014 this is the \"no-enlarge\" behaviour). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past the source). Applies only when width or height is set.",
1852
1862
  "honored_on": [
1853
1863
  "same_format",
1854
1864
  "format_change"
@@ -1861,7 +1871,7 @@
1861
1871
  ]
1862
1872
  },
1863
1873
  "height": {
1864
- "description": "Resize-inside-Output: target height in pixels (1-16384; width*height <= max_output_pixels). Resize replaces the image (one output). Optional \u2014 see `width`.",
1874
+ "description": "Target height in pixels (1-16384; width \u00d7 height within the group's max output area). Resizes the image (one output). Optional \u2014 see `width`.",
1865
1875
  "honored_on": [
1866
1876
  "same_format",
1867
1877
  "format_change"
@@ -1875,11 +1885,16 @@
1875
1885
  "depends_on": {
1876
1886
  "metadata": "strip"
1877
1887
  },
1878
- "description": "Selective per-category metadata keep (optimiser/same_format route): preserve ONLY these categories and strip the rest \u2014 refines the strip path (`metadata: strip`). Valid categories: `copyright`, `gps` (location), `date` (capture time); combinable. `planned` \u2014 the worker does not yet do selective keep (use `metadata: keep` to preserve everything), so the API returns feature_not_available until lambdas proves it.",
1888
+ "description": "Selectively keep only certain metadata categories and strip the rest \u2014 refines `metadata: strip`. Allowed categories: `copyright`, `gps` (location), `date` (capture time); combinable. `planned` \u2014 selective keep is not yet supported (use `metadata: keep` to preserve everything).",
1879
1889
  "honored_on": [
1880
1890
  "same_format"
1881
1891
  ],
1882
1892
  "items": {
1893
+ "enum": [
1894
+ "copyright",
1895
+ "gps",
1896
+ "date"
1897
+ ],
1883
1898
  "type": "string"
1884
1899
  },
1885
1900
  "type": "array",
@@ -1890,7 +1905,7 @@
1890
1905
  "depends_on": {
1891
1906
  "encoding_mode": "quality"
1892
1907
  },
1893
- "description": "Encode WebP losslessly (optimiser path): a genuine lossless WebP encode. STABLE \u2014 the compress-image worker honours the `lossless` bool (un-parked 2026-06-23). Note a lossless encode can be LARGER than a lossy one at the same visual quality. Only in `quality` mode \u2014 incompatible with `target_size` (the byte-budget loop is inherently lossy). Optimiser (same_format) route only.",
1908
+ "description": "Encode WebP losslessly: a genuine lossless WebP encode. Note a lossless encode can be LARGER than a lossy one at the same visual quality. Only in `quality` mode \u2014 incompatible with `target_size` (hitting a byte budget is inherently lossy).",
1894
1909
  "honored_on": [
1895
1910
  "same_format"
1896
1911
  ],
@@ -1898,7 +1913,7 @@
1898
1913
  },
1899
1914
  "metadata": {
1900
1915
  "default": "strip",
1901
- "description": "Metadata handling (optimiser/same_format route). `strip` = remove all EXIF/IPTC/XMP (default). `keep` = preserve EXIF/ICC/XMP \u2014 STABLE (worker-proven, libcaesium; lambdas PR #260, staging round-trip + unit test). `all` is a DEPRECATED alias of `strip` (the pre-2026-06-23 token; `all` reading as \"strip all\" was counterintuitive) \u2014 still accepted, emits Deprecation/Sunset, migrate to `strip`. The API lowers `strip`\u2192`all` at the worker boundary. Any other value is rejected as invalid_options.",
1916
+ "description": "Metadata handling. `strip` = remove all EXIF/IPTC/XMP (default). `keep` = preserve EXIF, colour profile, and XMP. `all` is a DEPRECATED alias of `strip` (kept for older callers; emits Deprecation/Sunset headers \u2014 migrate to `strip`). Any other value is rejected.",
1902
1917
  "honored_on": [
1903
1918
  "same_format"
1904
1919
  ],
@@ -1916,7 +1931,7 @@
1916
1931
  },
1917
1932
  "output_format": {
1918
1933
  "default": "original",
1919
- "description": "Output format. original = keep input format (default); webp = recompress to WebP \u2014 the one live compress+format target (proven on staging). Concrete format changes (jpeg/png/avif/gif/tiff) are the convert operation's job, not compress. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are SELECTION STRATEGIES, not concrete targets (convert cannot express \"pick the smallest\"), so they live here and stay `planned` until the API try-all-return-smallest logic + worker proof land. See FORMAT.md.",
1934
+ "description": "Output format. original = keep input format (default); webp = recompress to WebP. Concrete format changes (jpeg/png/avif/gif/tiff) are handled by the convert operation. `auto` / `smallest` are PLANNED auto-format directives \u2014 the server encodes to several candidate formats and returns the best (`auto` = the most suitable; `smallest` = the absolute fewest bytes), reporting the chosen format via the result `mime_type`. They are selection strategies, not concrete targets, and stay `planned` until supported. See FORMAT.md.",
1920
1935
  "per_value_availability": {
1921
1936
  "auto": {
1922
1937
  "availability": "planned"
@@ -1938,7 +1953,7 @@
1938
1953
  "depends_on": {
1939
1954
  "encoding_mode": "quality"
1940
1955
  },
1941
- "description": "Compression quality (1 = smallest file, 100 = best quality). Active in `quality` mode (default); in `target_size` mode the encode-measure loop owns quality, so this option is inactive.",
1956
+ "description": "Compression quality (1 = smallest file, 100 = best quality). Active in `quality` mode (default); in `target_size` mode the quality is chosen automatically to hit the size budget, so this option is inactive.",
1942
1957
  "honored_on": [
1943
1958
  "same_format"
1944
1959
  ],
@@ -1950,7 +1965,7 @@
1950
1965
  "depends_on": {
1951
1966
  "encoding_mode": "target_size"
1952
1967
  },
1953
- "description": "Target output size in bytes (min 1024 = 1 KiB). The image encode-measure loop binary-searches quality to land at or under the target; best-effort if the target is unreachable at min quality (the result reports the chosen quality + whether the target was met). STABLE (worker-proven, lambdas git-870a7c2). Optimiser (same_format) route.",
1968
+ "description": "Target output size in bytes (min 1024 = 1 KiB). The encoder searches for the highest quality that lands at or under this size; if the target can't be met even at the lowest quality, the closest result is returned (the result reports the chosen quality and whether the target was met). Requires `encoding_mode: target_size`.",
1954
1969
  "honored_on": [
1955
1970
  "same_format"
1956
1971
  ],
@@ -1958,7 +1973,7 @@
1958
1973
  "type": "integer"
1959
1974
  },
1960
1975
  "width": {
1961
- "description": "Resize-inside-Output: target width in pixels (1-16384; width*height <= max_output_pixels). Resize the image as part of Output (replace semantics, ONE transformed output) using the proven convert resize engine (Lanczos; the API routes Output resize through the convert worker, BOTH routes \u2014 the compress optimiser has no resize) \u2014 it is NOT the derivative `thumbnail` op. Optional; omit to keep the source dimensions.",
1976
+ "description": "Target width in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Resizes the image as part of the operation (one transformed output, replacing the source). Optional; omit to keep the source dimensions.",
1962
1977
  "honored_on": [
1963
1978
  "same_format",
1964
1979
  "format_change"
@@ -1984,7 +1999,7 @@
1984
1999
  "vorbis"
1985
2000
  ]
1986
2001
  },
1987
- "description": "Audio track bitrate in kbps. Only valid with an explicit re-encode audio_codec (aac/opus/vorbis): an explicit audio_bitrate with audio_codec `copy`/omitted is rejected as `invalid_options` at workflow-create (the API enforces this `depends_on`); copy preserves the source bitrate. FE/SDK also prune it pre-submit. No default: a bitrate is only meaningful on an explicit re-encode (matches the worker, which applies none).\n",
2002
+ "description": "Audio track bitrate in kbps. Only valid with an explicit re-encode audio_codec (aac/opus/vorbis): an explicit audio_bitrate with audio_codec `copy`/omitted is rejected; copy preserves the source bitrate. No default: a bitrate is only meaningful on an explicit re-encode.\n",
1988
2003
  "type": "enum",
1989
2004
  "value_type": "integer",
1990
2005
  "values": [
@@ -1998,7 +2013,7 @@
1998
2013
  },
1999
2014
  "audio_codec": {
2000
2015
  "default": "copy",
2001
- "description": "Audio codec. copy = passthrough (keep the source stream as-is; re-encode only on explicit request) \u2014 aac/opus/vorbis re-encode. Default is `copy` (ADR-0020 D7, carried into ADR-0023): on the same-container path (`output_format` original/absent) the source stream is already container-compatible, so copy is safe. When `output_format` RE-TARGETS the container, an absent/default audio_codec resolves to a container-valid codec via the facade (e.g. webm -> opus), NOT a blind copy of an incompatible stream \u2014 a DEFAULT is never rejected (the exact per-container resolution, incl. ogg vorbis-vs-opus, is an output_format stable-flip prerequisite, ADR-0023 D5). A caller-EXPLICIT codec the resolved container cannot carry (e.g. aac on a webm target) returns invalid_options. See FORMAT.md \"Container-conditional defaults\" + ADR-0023 (supersedes ADR-0020).\n",
2016
+ "description": "Audio codec. copy = passthrough (keep the source stream as-is; re-encode only on explicit request) \u2014 aac/opus/vorbis re-encode. Default is `copy`: on the same-container path (`output_format` original/absent) the source stream is already container-compatible, so copy is safe. When `output_format` RE-TARGETS the container, an absent/default audio_codec resolves to a container-valid codec via the facade (e.g. webm -> opus), NOT a blind copy of an incompatible stream \u2014 a DEFAULT is never rejected (the exact per-container resolution, incl. ogg vorbis-vs-opus, is an output_format stable-flip prerequisite). A caller-EXPLICIT codec the resolved container cannot carry (e.g. aac on a webm target) is rejected. See FORMAT.md \"Container-conditional defaults\".\n",
2002
2017
  "type": "enum",
2003
2018
  "values": [
2004
2019
  "aac",
@@ -2009,7 +2024,7 @@
2009
2024
  },
2010
2025
  "codec": {
2011
2026
  "default": "h264",
2012
- "description": "Video codec. h264 = widest compatibility, h265/av1 = better compression but slower. The default (h264) is the MP4/MOV standard and is CONTAINER-CONDITIONAL against the RESOLVED output container (the explicit `output_format` if set, else the input container): for a container that cannot carry it (e.g. WebM, which carries VP8/VP9/ AV1) the server resolves a container-valid codec (WebM -> vp9). A DEFAULT value is never rejected \u2014 including an explicit `output_format: webm` left at the default `h264` (resolves to vp9, NOT invalid_options); only a caller-EXPLICIT incompatible codec returns invalid_options. See FORMAT.md \"Container-conditional defaults\" + ADR-0023 (supersedes ADR-0020).\n",
2027
+ "description": "Video codec. h264 = widest compatibility, h265/av1 = better compression but slower. The default (h264) is the MP4/MOV standard and is CONTAINER-CONDITIONAL against the RESOLVED output container (the explicit `output_format` if set, else the input container): for a container that cannot carry it (e.g. WebM, which carries VP8/VP9/ AV1) the server resolves a container-valid codec (WebM -> vp9). A DEFAULT value is never rejected \u2014 including an explicit `output_format: webm` left at the default `h264` (resolves to vp9, not rejected); only a caller-EXPLICIT incompatible codec is rejected. See FORMAT.md \"Container-conditional defaults\".\n",
2013
2028
  "type": "enum",
2014
2029
  "values": [
2015
2030
  "h264",
@@ -2030,7 +2045,7 @@
2030
2045
  },
2031
2046
  "encoding_mode": {
2032
2047
  "default": "crf",
2033
- "description": "crf = constant quality (variable file size). target_size = constrained output file size (two-pass). target_size is LIVE for mp4/MOV output; for webm/ogg output and long-form the server returns `feature_not_available` (use crf there) until VP9 two-pass ships.",
2048
+ "description": "crf = constant quality (variable file size). target_size = constrained output file size (two-pass). target_size is LIVE for mp4/MOV output; for webm/ogg output and long-form the server returns `feature_not_available` (use crf there) until two-pass for this container ships.",
2034
2049
  "type": "enum",
2035
2050
  "values": [
2036
2051
  "crf",
@@ -2039,7 +2054,7 @@
2039
2054
  },
2040
2055
  "faststart": {
2041
2056
  "default": true,
2042
- "description": "Move the MP4/MOV moov atom to start for progressive web playback. MP4/MOV only. CONTAINER-CONDITIONAL against the RESOLVED output container (the explicit `output_format` if set, else the input): the default (true) is silently inapplicable for a container with no moov atom (e.g. WebM) \u2014 that is NOT a rejection. Only a caller-EXPLICIT faststart:true on such a container returns invalid_options. See FORMAT.md \"Container-conditional defaults\" + ADR-0023 (supersedes ADR-0020).\n",
2057
+ "description": "Move the MP4/MOV moov atom to the front so the file starts playing before it is fully downloaded (progressive web playback). MP4/MOV only. CONTAINER-CONDITIONAL against the RESOLVED output container (the explicit `output_format` if set, else the input): the default (true) is silently inapplicable for a container with no moov atom (e.g. WebM) \u2014 that is NOT a rejection. Only a caller-EXPLICIT faststart:true on such a container is rejected. See FORMAT.md \"Container-conditional defaults\".\n",
2043
2058
  "type": "boolean"
2044
2059
  },
2045
2060
  "fit": {
@@ -2065,14 +2080,14 @@
2065
2080
  "type": "float"
2066
2081
  },
2067
2082
  "height": {
2068
- "description": "Output height in pixels. Must be an EVEN integer >= 2 (yuv420p chroma-subsampling); odd values are rejected as invalid_options by the encoder.",
2083
+ "description": "Output height in pixels. Must be an even integer \u2265 2; odd values are rejected by the encoder.",
2069
2084
  "max": 4320,
2070
2085
  "min": 2,
2071
2086
  "type": "integer"
2072
2087
  },
2073
2088
  "output_format": {
2074
2089
  "default": "original",
2075
- "description": "Output container. original = keep the input container (default, same-format compression). Non-`original` values re-target the container via the API \"compress+format\" facade (one canonicalized convert-with-size-cap pass); see per_value_availability + ADR-0023 + FORMAT.md. When set, the RESOLVED target container determines the valid codec/audio_codec/faststart sets \u2014 an absent/default codec resolves to a container-valid one (e.g. webm -> vp9) and is never rejected; only a caller-EXPLICIT incompatible value is invalid_options.",
2090
+ "description": "Output container. original = keep the input container (default, same-format compression). Non-`original` values re-target the container via the API \"compress+format\" facade (one canonicalized convert-with-size-cap pass); see FORMAT.md. When set, the RESOLVED target container determines the valid codec/audio_codec/faststart sets \u2014 an absent/default codec resolves to a container-valid one (e.g. webm -> vp9) and is never rejected; only a caller-EXPLICIT incompatible value is rejected.",
2076
2091
  "per_value_availability": {
2077
2092
  "mp4": {
2078
2093
  "availability": "planned"
@@ -2112,22 +2127,22 @@
2112
2127
  "depends_on": {
2113
2128
  "encoding_mode": "target_size"
2114
2129
  },
2115
- "description": "Target output file size in bytes (min 1MiB). The libx264 two-pass encode lands the output at or under the target (a safety undershoot + audio reserve apply). mp4/MOV output only \u2014 webm/ogg + long-form return `feature_not_available` until VP9 two-pass ships.",
2130
+ "description": "Target output file size in bytes (min 1MiB). A two-pass encode lands the output at or under the target (a safety undershoot + audio reserve apply). mp4/MOV output only \u2014 webm/ogg + long-form return `feature_not_available` until two-pass for this container ships.",
2116
2131
  "min": 1048576,
2117
2132
  "type": "integer"
2118
2133
  },
2119
2134
  "trim_end": {
2120
- "description": "Trim from end in seconds (absent = to the end). Removes this many seconds from the end; with `trim_start` the kept clip is [`trim_start`, input_duration \u2212 `trim_end`]. STABLE \u2014 see `trim_start` (short-form only; long-form \u2192 `feature_not_available`).\n",
2135
+ "description": "Trim from end in seconds (absent = to the end). Removes this many seconds from the end; with `trim_start` the kept clip is [`trim_start`, input_duration \u2212 `trim_end`]. See `trim_start` (short-form only; long-form is not supported).\n",
2121
2136
  "min": 0,
2122
2137
  "type": "float"
2123
2138
  },
2124
2139
  "trim_start": {
2125
- "description": "Trim from beginning in seconds (absent = from the start). STABLE (qZI5EK9j) \u2014 the compress-video worker cuts the clip before encoding (input-side ffmpeg `-ss`/`-t`; lambdas PR #258), SAME FROM-END semantic as convert/merge: the kept clip is [`trim_start`, input_duration \u2212 `trim_end`]. SHORT-FORM only \u2014 for long-form video the server returns `feature_not_available` (trim a short clip, or chain a `split`); see the compress.video.trim.longform constraint.\n",
2140
+ "description": "Trim from beginning in seconds (absent = from the start). The clip is cut before encoding, with the same from-end semantic as convert/merge: the kept clip is [`trim_start`, input_duration \u2212 `trim_end`]. SHORT-FORM only \u2014 for long-form video this is not supported (trim a short clip, or chain a `split`); see the compress.video.trim.longform constraint.\n",
2126
2141
  "min": 0,
2127
2142
  "type": "float"
2128
2143
  },
2129
2144
  "width": {
2130
- "description": "Output width in pixels. Must be an EVEN integer >= 2 (yuv420p chroma-subsampling); odd values are rejected as invalid_options by the encoder.",
2145
+ "description": "Output width in pixels. Must be an even integer \u2265 2; odd values are rejected by the encoder.",
2131
2146
  "max": 7680,
2132
2147
  "min": 2,
2133
2148
  "type": "integer"
@@ -2236,7 +2251,7 @@
2236
2251
  },
2237
2252
  "pages": {
2238
2253
  "default": "1",
2239
- "description": "Page selection (e.g. '1-5,8'). Each resolved page produces a\nseparate output file. **Maximum 200 resolved entries per\nrequest** \u2014 aligns with the `OperationResult.outputs[]`\n`maxItems: 200` cap per ADR-0009 \u00a7D5\n(`docs/decisions/0009-multi-output-result-envelope.md`).\nExpressions resolving to more than 200 entries (e.g.\n`'1-201'`, `'1-100,102-205'`) are out of contract.\nDuplicate or overlapping entries (e.g. `'1,1'`,\n`'1-100,50-150'`) are counted toward the cap as parsed;\ndeduplication, if any, is a runtime concern and is NOT part\nof this contract. Enforcement at the API edge validator and\nthe Lambda runtime is tracked under WgCqnMRa follow-ups\n(cross-repo: API + Lambdas `bFlEXizR`).\n",
2254
+ "description": "Page selection (e.g. '1-5,8'). Each resolved page produces a\nseparate output file. **Maximum 200 resolved entries per\nrequest** \u2014 aligns with the `OperationResult.outputs[]`\n`maxItems: 200` cap. Expressions resolving to more than 200\nentries (e.g. `'1-201'`, `'1-100,102-205'`) are out of\ncontract. Duplicate or overlapping entries (e.g. `'1,1'`,\n`'1-100,50-150'`) are counted toward the cap as parsed;\ndeduplication, if any, is a runtime concern and is NOT part\nof this contract.\n",
2240
2255
  "type": "string"
2241
2256
  }
2242
2257
  }
@@ -2256,9 +2271,8 @@
2256
2271
  ],
2257
2272
  "options": {
2258
2273
  "auto_orient": {
2259
- "availability": "planned",
2260
2274
  "default": true,
2261
- "description": "Bake EXIF orientation into the pixels on the transcoder (format-change) route \u2014 mirrors compress.image.auto_orient. `planned` \u2014 the convert worker does not yet apply the orientation transform. Declared HERE so the API re-validates the rewritten Output convert op.",
2275
+ "description": "Bake EXIF orientation into the pixels (honour the camera/device rotation tag) so the output displays upright everywhere, then drop the now-redundant orientation tag. Default `true`; no-op for inputs without an orientation tag. The format-change re-encodes from pixels, so other (non-orientation) metadata is not carried across.",
2262
2276
  "honored_on": [
2263
2277
  "format_change"
2264
2278
  ],
@@ -2268,7 +2282,7 @@
2268
2282
  "depends_on": {
2269
2283
  "output_format": "jpeg"
2270
2284
  },
2271
- "description": "Background colour as a 6-digit hex (`#RRGGBB`, e.g. `#ffffff`) for transparent images converted to JPEG (the format-change\u2192JPEG alpha-flatten \u2014 Output's transparency fill control). HEX ONLY: the worker honours only `#RRGGBB` and silently drops a CSS named colour (e.g. `magenta`) to the white default, so the `pattern` rejects a non-hex value as `invalid_options` rather than advertising an inert free-form string (e2e staging-verified 2026-06-22).",
2285
+ "description": "Background colour as a 6-digit hex (`#RRGGBB`, e.g. `#ffffff`) for transparent images converted to JPEG (the alpha-flatten fill colour when converting to JPEG). HEX ONLY: a non-hex value (e.g. a CSS named colour) is rejected rather than silently falling back.",
2272
2286
  "honored_on": [
2273
2287
  "format_change"
2274
2288
  ],
@@ -2278,7 +2292,7 @@
2278
2292
  "color_profile": {
2279
2293
  "availability": "planned",
2280
2294
  "default": "keep",
2281
- "description": "ICC colour-profile handling on the transcoder (format-change) route \u2014 mirrors compress.image.color_profile (its own control, decoupled from metadata). `keep` (default) = preserve; `srgb` = convert to sRGB (incl. CMYK\u2192sRGB); `strip` = drop. `planned` \u2014 the convert worker re-encodes from pixels and does not yet preserve/convert the profile. Declared HERE so the API re-validates the rewritten Output convert op.",
2295
+ "description": "Colour-profile handling \u2014 a dedicated control, separate from `metadata` so that stripping metadata never silently shifts colours. `keep` (default) = preserve the embedded colour profile; `srgb` = convert to sRGB (also handles CMYK \u2192 sRGB); `strip` = drop the profile. `planned` \u2014 not yet supported.",
2282
2296
  "honored_on": [
2283
2297
  "format_change"
2284
2298
  ],
@@ -2296,7 +2310,7 @@
2296
2310
  "logic": "or",
2297
2311
  "width": "set"
2298
2312
  },
2299
- "description": "Resize mode. `max` (default) = fit within bounds, preserving aspect ratio and NEVER upscaling \u2014 it caps at the source dimensions. This IS the \"no-enlarge\" behaviour (there is no separate `no_enlarge` flag \u2014 the worker has no such key). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past source). Applies only when width or height is set.",
2313
+ "description": "Resize mode. `max` (default) = fit within the given bounds, preserving aspect ratio and never upscaling (caps at the source dimensions \u2014 this is the \"no-enlarge\" behaviour). `crop`/`scale` = exact dimensions (`scale` deliberately enlarges past the source). Applies only when width or height is set.",
2300
2314
  "honored_on": [
2301
2315
  "format_change"
2302
2316
  ],
@@ -2308,7 +2322,7 @@
2308
2322
  ]
2309
2323
  },
2310
2324
  "height": {
2311
- "description": "Resize-inside-Output: target height in pixels (1-16384; width*height <= max_output_pixels). Not honored for SVG input. See `width`.",
2325
+ "description": "Target height in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Not honored for SVG input. See `width`.",
2312
2326
  "honored_on": [
2313
2327
  "format_change"
2314
2328
  ],
@@ -2319,7 +2333,7 @@
2319
2333
  "metadata": {
2320
2334
  "availability": "planned",
2321
2335
  "default": "strip",
2322
- "description": "Metadata handling on the format-change (convert/transcoder) route. `strip` = remove all EXIF/IPTC/XMP (default), `keep` = preserve. `planned` \u2014 the convert worker re-encodes from pixels and does not yet preserve metadata across a format change, so the API returns feature_not_available (rather than \"Unknown option\" on a format-change request \u2014 e2e/api flagged 2026-06-23). This is a new (planned) surface, so it carries no deprecated `all` alias \u2014 unlike compress.image.metadata, which keeps `all` for its existing callers. Stable-flip gated on lambdas proving convert-route metadata preservation; the honoured target subset may narrow (avif/heic targets re-encode and cannot keep).",
2336
+ "description": "Metadata handling across a format change. `strip` = remove all EXIF/IPTC/XMP (default), `keep` = preserve. `planned` \u2014 metadata is not yet preserved across a format change. The honoured target subset may narrow (avif/heic targets are re-encoded and cannot keep metadata).",
2323
2337
  "honored_on": [
2324
2338
  "format_change"
2325
2339
  ],
@@ -2360,7 +2374,7 @@
2360
2374
  "type": "integer"
2361
2375
  },
2362
2376
  "width": {
2363
- "description": "Resize-inside-Output: target width in pixels (1-16384; width*height <= max_output_pixels). The convert worker resizes (Lanczos) before encode; single transformed output. NOT honored for SVG input (the worker rejects svg resize). Input-gated \u2014 see the resize comment above.",
2377
+ "description": "Target width in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Resizes the image before encoding (one transformed output). NOT honored for SVG input. Optional; omit to keep the source dimensions.",
2364
2378
  "honored_on": [
2365
2379
  "format_change"
2366
2380
  ],
@@ -2388,7 +2402,6 @@
2388
2402
  ],
2389
2403
  "options": {
2390
2404
  "auto_orient": {
2391
- "availability": "planned",
2392
2405
  "default": true,
2393
2406
  "depends_on": {
2394
2407
  "output_format": [
@@ -2400,7 +2413,7 @@
2400
2413
  "tiff"
2401
2414
  ]
2402
2415
  },
2403
- "description": "Bake EXIF orientation into the pixels (format-change route) \u2014 mirrors compress.image.auto_orient. `planned`. SCOPED via depends_on to the STILL-IMAGE targets only \u2014 the video targets (mp4/webm) carry no EXIF orientation. Declared HERE so the API re-validates the rewritten Output op.",
2416
+ "description": "Bake EXIF orientation into the pixels so the output displays upright everywhere. Default `true`; no-op for inputs without an orientation tag (a GIF carries none, so this is a no-op for GIF input). Scoped to the STILL-IMAGE targets only \u2014 the video targets (mp4/webm) carry no EXIF orientation.",
2404
2417
  "honored_on": [
2405
2418
  "format_change"
2406
2419
  ],
@@ -2410,7 +2423,7 @@
2410
2423
  "depends_on": {
2411
2424
  "output_format": "jpeg"
2412
2425
  },
2413
- "description": "Background colour as a 6-digit hex (`#RRGGBB`) for a transparent GIF flattened to JPEG (alpha-flatten). HEX ONLY \u2014 non-hex is rejected as `invalid_options` (see convert.image.background).",
2426
+ "description": "Background colour as a 6-digit hex (`#RRGGBB`) for a transparent GIF flattened to JPEG (alpha-flatten). HEX ONLY \u2014 a non-hex value is rejected (see convert.image.background).",
2414
2427
  "honored_on": [
2415
2428
  "format_change"
2416
2429
  ],
@@ -2430,7 +2443,7 @@
2430
2443
  "tiff"
2431
2444
  ]
2432
2445
  },
2433
- "description": "ICC colour-profile handling (format-change route) \u2014 mirrors compress.image.color_profile. `keep` (default)/`srgb`/`strip`. `planned`. SCOPED via depends_on to the STILL-IMAGE targets only \u2014 the video targets (mp4/webm) carry no ICC, so color_profile is not accepted for a gif\u2192video output (it would be inert). Declared HERE so the API re-validates the rewritten Output op.",
2446
+ "description": "Colour-profile handling \u2014 `keep` (default) = preserve the embedded colour profile; `srgb` = convert to sRGB; `strip` = drop the profile. `planned` \u2014 not yet supported. Scoped to the STILL-IMAGE targets only \u2014 the video targets (mp4/webm) carry no colour profile, so this is not accepted for a gif\u2192video output.",
2434
2447
  "honored_on": [
2435
2448
  "format_change"
2436
2449
  ],
@@ -2470,7 +2483,7 @@
2470
2483
  "tiff"
2471
2484
  ]
2472
2485
  },
2473
- "description": "Resize-inside-Output: target height in pixels (1-16384; width*height <= max_output_pixels). Scoped to the still-image targets \u2014 resized gif\u2192video unproven, see `width`.",
2486
+ "description": "Target height in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Scoped to the still-image targets \u2014 see `width`.",
2474
2487
  "honored_on": [
2475
2488
  "format_change"
2476
2489
  ],
@@ -2481,7 +2494,7 @@
2481
2494
  "metadata": {
2482
2495
  "availability": "planned",
2483
2496
  "default": "strip",
2484
- "description": "Metadata handling on the format-change route (still-image targets). `planned` \u2014 mirrors convert.image.metadata (the convert worker does not yet preserve metadata across a format change). The video targets carry no EXIF/IPTC/XMP.",
2497
+ "description": "Metadata handling for the still-image targets. `planned` \u2014 metadata is not yet preserved across a format change. The video targets carry no EXIF/IPTC/XMP.",
2485
2498
  "honored_on": [
2486
2499
  "format_change"
2487
2500
  ],
@@ -2492,7 +2505,7 @@
2492
2505
  ]
2493
2506
  },
2494
2507
  "output_format": {
2495
- "description": "Target format. The still-image targets (jpeg/png/webp/avif/gif/tiff) take the first frame / rasterise. `mp4` / `webm` are the GIF\u2192VIDEO transcode targets \u2014 the animated GIF becomes a muted, play-once video (`mp4` = H.264 / yuv420p / +faststart, plays everywhere; `webm` = VP9, smaller). STABLE \u2014 worker-proven on staging (lambdas PR #268, deployed git-6783cb9: gif\u2192mp4 ~70% smaller / gif\u2192webm ~84% smaller, valid containers + worker-set Content-Type, real decode-every-frame transcode). GIFs are silent (no audio stream); the GIF infinite-loop becomes a playback attribute (`<video loop>`) the player sets, NOT baked into the file.",
2508
+ "description": "Target format. The still-image targets (jpeg/png/webp/avif/gif/tiff) take the first frame / rasterise. `mp4` / `webm` are the GIF\u2192VIDEO transcode targets \u2014 the animated GIF becomes a muted, play-once video (`mp4` = H.264, plays everywhere; `webm` = VP9, smaller), typically far smaller than the original GIF. GIFs are silent (no audio stream); the GIF infinite-loop becomes a playback attribute (`<video loop>`) the player sets, NOT baked into the file.",
2496
2509
  "required": true,
2497
2510
  "type": "enum",
2498
2511
  "values": [
@@ -2515,7 +2528,7 @@
2515
2528
  "avif"
2516
2529
  ]
2517
2530
  },
2518
- "description": "Output quality for the lossy STILL-IMAGE targets (jpeg/webp/avif). The video targets (mp4/webm) use a sensible default encode quality in v1 \u2014 there is no exposed CRF knob yet (planned-first; may be added later).",
2531
+ "description": "Output quality for the lossy STILL-IMAGE targets (jpeg/webp/avif). The video targets (mp4/webm) use a sensible default encode quality in v1 \u2014 there is no exposed CRF knob yet (may be added later).",
2519
2532
  "honored_on": [
2520
2533
  "format_change"
2521
2534
  ],
@@ -2534,7 +2547,7 @@
2534
2547
  "tiff"
2535
2548
  ]
2536
2549
  },
2537
- "description": "Resize-inside-Output: target width in pixels (1-16384; width*height <= max_output_pixels). SCOPED via depends_on to the STILL-IMAGE targets \u2014 the bare gif\u2192video transcode proof (#268) did NOT prove resize-while-transcoding, so width/height are not accepted for the mp4/webm video targets (prove-before- flip; else the API would accept+route+bill a resized gif\u2192video the worker may not honour). Un-scope per-target when lambdas proves resized gif\u2192video.",
2550
+ "description": "Target width in pixels (1-16384; width \u00d7 height must not exceed the group's maximum output area). Scoped to the STILL-IMAGE targets only \u2014 resizing while transcoding to the mp4/webm video targets is not yet supported, so width/height are not accepted for the video targets.",
2538
2551
  "honored_on": [
2539
2552
  "format_change"
2540
2553
  ],
@@ -2553,7 +2566,7 @@
2553
2566
  "depends_on": {
2554
2567
  "output_format": "jpeg"
2555
2568
  },
2556
- "description": "Background colour as a 6-digit hex (`#RRGGBB`) for transparent SVG rasterised to JPEG (alpha-flatten). HEX ONLY \u2014 non-hex is rejected as `invalid_options` (see convert.image.background).",
2569
+ "description": "Background colour as a 6-digit hex (`#RRGGBB`) for transparent SVG rasterised to JPEG (alpha-flatten). HEX ONLY \u2014 a non-hex value is rejected (see convert.image.background).",
2557
2570
  "honored_on": [
2558
2571
  "format_change"
2559
2572
  ],
@@ -2618,7 +2631,7 @@
2618
2631
  "depends_on": {
2619
2632
  "output_format": "gif"
2620
2633
  },
2621
- "description": "GIF dithering method (ffmpeg `paletteuse`). `bayer` is a good size/quality balance; `none` is smallest (may band).\n",
2634
+ "description": "GIF dithering method. `bayer` is a good size/quality balance; `none` is smallest (may band).\n",
2622
2635
  "type": "enum",
2623
2636
  "values": [
2624
2637
  "none",
@@ -2633,7 +2646,7 @@
2633
2646
  "depends_on": {
2634
2647
  "output_format": "gif"
2635
2648
  },
2636
- "description": "Output frame rate for video \u2192 GIF (ffmpeg `fps` filter). Lower = fewer frames = smaller file. Video \u2192 GIF only.\n",
2649
+ "description": "Output frame rate for video \u2192 GIF. Lower = fewer frames = smaller file. Video \u2192 GIF only.\n",
2637
2650
  "max": 50,
2638
2651
  "min": 1,
2639
2652
  "type": "float"
@@ -2652,7 +2665,7 @@
2652
2665
  "depends_on": {
2653
2666
  "output_format": "gif"
2654
2667
  },
2655
- "description": "GIF palette size (ffmpeg `palettegen` max_colors). Fewer colors = smaller file.\n",
2668
+ "description": "GIF palette size (the palette generation max colours). Fewer colors = smaller file.\n",
2656
2669
  "max": 256,
2657
2670
  "min": 2,
2658
2671
  "type": "integer"
@@ -2674,7 +2687,7 @@
2674
2687
  "type": "float"
2675
2688
  },
2676
2689
  "trim_start": {
2677
- "description": "Trim from beginning in seconds (absent = from the start). The convert-video worker cuts the clip before transcoding \u2014 the user's \"trim a video then make a GIF\" path. The kept clip is [`trim_start`, input_duration \u2212 `trim_end`]. Live for all video outputs (gif/mp4/webm/ogg).\n",
2690
+ "description": "Trim from beginning in seconds (absent = from the start). The clip is cut before transcoding \u2014 the user's \"trim a video then make a GIF\" path. The kept clip is [`trim_start`, input_duration \u2212 `trim_end`]. Live for all video outputs (gif/mp4/webm/ogg).\n",
2678
2691
  "min": 0,
2679
2692
  "type": "float"
2680
2693
  },
@@ -2720,7 +2733,7 @@
2720
2733
  "custom_luma": {
2721
2734
  "availability": "planned",
2722
2735
  "default": false,
2723
- "description": "Apply a caller-uploaded luma matte (transition-mask) over a base\nvideo to produce a custom luma-matte transition effect. Inspired\nby Cloudinary's deprecated luma-matte custom transitions and\nintended as a paid-tier (`pro`+) creative feature.\n\n**NOT** related to FFmpeg's `xfade=custom` filter (which is an\nexpression-mode parameter on the same `xfade` filter). The\n`merge.transition.custom` value is tagged `planned` as an advisory\nthat points callers at this dedicated operation for caller-uploaded\nluma-matte transitions.\n\nMulti-input: exactly one input with `role: base` (the source\nvideo being transitioned) + exactly one input with\n`role: transition_mask` (the luma matte \u2014 a video whose pixel\nluminance drives the transition reveal \u2014 bright = revealed,\ndark = held). Each input is a `MultiInputSource` \u2014 imported\nexternally, referenced from a vault connection, or an upstream\n`job_output` (uploads are NOT referenced directly inside inputs[]).\nAn uploaded base or mask enters via a `passthrough` source job\nreferenced by `{ type: job_output, from: <id> }` (per ticket\n4som89Uh).\n\nPer ADR-0001 \u00a71.3 (Tension 1 \u2014 `planned` operations return\n`feature_not_available` HTTP 422 until Lambda support ships).\nPer ADR-0004 \u00a7\"V2 JobDefinition\" + plan v5 \u00a7F4 round 6\n(codex-corrected \u2014 `xfade=custom` is an expression, not a\ntransition; this operation is the deliberate alternative).\n",
2736
+ "description": "Apply a caller-uploaded luma matte (transition-mask) over a base\nvideo to produce a custom luma-matte transition effect. Inspired\nby Cloudinary's deprecated luma-matte custom transitions and\nintended as a paid-tier (`pro`+) creative feature.\n\n**NOT** related to the `xfade=custom` expression-mode transition\nparameter. The `merge.transition.custom` value is tagged `planned`\nas an advisory that points callers at this dedicated operation for\ncaller-uploaded luma-matte transitions.\n\nMulti-input: exactly one input with `role: base` (the source\nvideo being transitioned) + exactly one input with\n`role: transition_mask` (the luma matte \u2014 a video whose pixel\nluminance drives the transition reveal \u2014 bright = revealed,\ndark = held). Each input is imported\nexternally, referenced from a vault connection, or an upstream\n`job_output` (uploads are NOT referenced directly inside inputs[]).\nAn uploaded base or mask enters via a `passthrough` source job\nreferenced by `{ type: job_output, from: <id> }` (per ticket\n4som89Uh).\n\n`planned` \u2014 not yet supported.\n",
2724
2737
  "input_model": "multi",
2725
2738
  "max_inputs": 2,
2726
2739
  "mime_groups": {
@@ -2807,11 +2820,11 @@
2807
2820
  },
2808
2821
  "image_watermark": {
2809
2822
  "default": false,
2810
- "description": "Apply an image overlay onto a base media\nasset. Multi-input: exactly one input with role: base (the source\nasset) + exactly one with role: overlay (the watermark). Each input\nis a `MultiInputSource` \u2014 an external_import handle, a vault\nconnection, or an upstream `job_output` (uploads are NOT referenced\ndirectly inside inputs[]). To use an uploaded base or overlay, feed\nit through a `passthrough` source job and reference that job via\n`{ type: job_output, from: <id> }` (per ticket 4som89Uh).\n\nStable today for static-image bases (image/jpeg, image/png,\nimage/webp); animated GIF bases are advertised as `planned` via\nthe parallel `image_gif` mime_group \u2014 schema is contract-defined,\ndispatch returns `feature_not_available` (422) until Lambda\nsupport ships. Per Tension 1 (ADR-0001) + plan v5 \u00a7F4 round 6.\n\n**Video bases are NOT supported by `image_watermark`.** Use the\ndedicated `video_watermark` operation\n(`schemas/operations/video_watermark.yaml`) which routes through\nFFmpeg overlay + re-encode. Per ADR-0013.\n\n**Audio is explicitly NOT supported.** For audio overlay (DJ tags,\npodcast intros, jingles), use the `audio_overlay` operation\n(declared in schemas/operations/audio_overlay.yaml at\n`availability: planned`).\n\nSource capped at 100 megapixels for image bases (raised from 25MP \u2014\nlambdas #250, proven on the 45.9MP report case); per-frame caps for\nanimated GIF bases will be defined when the corresponding Lambda\nsupport ships. Per ADR-0004 \u00a7\"V2 JobDefinition\" + plan v5 \u00a7F1.\n",
2823
+ "description": "Apply an image overlay onto a base media\nasset. Multi-input: exactly one input with role: base (the source\nasset) + exactly one with role: overlay (the watermark). Each input\nis an external_import handle, a vault\nconnection, or an upstream `job_output` (uploads are NOT referenced\ndirectly inside inputs[]). To use an uploaded base or overlay, feed\nit through a `passthrough` source job and reference that job via\n`{ type: job_output, from: <id> }` (per ticket 4som89Uh).\n\nStable today for static-image bases (image/jpeg, image/png,\nimage/webp); animated GIF bases are advertised as `planned` via\nthe parallel `image_gif` mime_group \u2014 schema is contract-defined,\ndispatch returns `feature_not_available` (422) until backend\nsupport ships.\n\nTIFF (image/tiff) and BMP (image/bmp) bases are likewise advertised\nas `planned` via the parallel `image_tiff` / `image_bmp` mime_groups.\nBoth formats are already decodable and the worker's encode arms are a\ncheap add (shared `image`-crate encoders), so each flips to `stable`\nonce its arm ships and is proven. Because watermark output mirrors the\ninput format (`produces: same_as_input`), advertising a base format is\nonly honest when the worker can both decode AND encode it \u2014 which\nholds for TIFF/BMP but not for HEIC/AVIF (no encoder) or SVG (cannot\nre-emit svg from a rasterised composite); those stay out of scope.\n\n**Video bases are NOT supported by `image_watermark`.** Use the\ndedicated `video_watermark` operation\n(`schemas/operations/video_watermark.yaml`), which overlays and\nre-encodes the video.\n\n**Audio is explicitly NOT supported.** For audio overlay (DJ tags,\npodcast intros, jingles), use the `audio_overlay` operation\n(declared in schemas/operations/audio_overlay.yaml at\n`availability: planned`).\n\nSource capped at 100 megapixels for image bases; per-frame caps for\nanimated GIF bases will be defined when the corresponding backend\nsupport ships.\n",
2811
2824
  "features": {
2812
2825
  "multi_overlay_stack": {
2813
2826
  "availability": "planned",
2814
- "description": "Allow up to 8 overlay inputs per job (currently capped at 1\noverlay). Per-overlay placement via JobInputV2.per_input_options.\nLambda-side support not yet confirmed; tagged planned per\nADR-0001 \u00a71.4. Scope is image-base only at the time this feature\nwas specified; extension to GIF / video bases tracks alongside\nthe corresponding mime_group `planned` \u2192 `stable` flips.\n"
2827
+ "description": "Allow up to 8 overlay inputs per job (currently capped at 1\noverlay). Per-overlay placement via per-input placement options.\nBackend support not yet confirmed; tagged planned. Scope is\nimage-base only at the time this feature was specified; extension\nto GIF / video bases tracks alongside the corresponding mime_group\n`planned` \u2192 `stable` flips.\n"
2815
2828
  }
2816
2829
  },
2817
2830
  "input_model": "multi",
@@ -2866,6 +2879,54 @@
2866
2879
  }
2867
2880
  }
2868
2881
  },
2882
+ "image_bmp": {
2883
+ "availability": "planned",
2884
+ "mimes": [
2885
+ "image/bmp"
2886
+ ],
2887
+ "options": {
2888
+ "anchor": {
2889
+ "default": "bottom_right",
2890
+ "description": "9-grid anchor position on the base image. Combined with\nmargin_x and margin_y for fine-grained offset from the\nanchor point.\n",
2891
+ "type": "enum",
2892
+ "values": [
2893
+ "top_left",
2894
+ "top_center",
2895
+ "top_right",
2896
+ "center_left",
2897
+ "center",
2898
+ "center_right",
2899
+ "bottom_left",
2900
+ "bottom_center",
2901
+ "bottom_right"
2902
+ ]
2903
+ },
2904
+ "margin_x": {
2905
+ "default": "0px",
2906
+ "description": "Horizontal offset from the anchor in pixels or percentage of\nbase width. Anchor-relative; positive moves toward the image\ncentre.\n\n**`anchor: center` exception.** Same rejection rule as the\n`image` mime_group \u2014 non-zero `margin_x` with\n`anchor: center` is rejected by the API at workflow-create\ntime as `invalid_options` (HTTP 422).\n",
2907
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
2908
+ "type": "string"
2909
+ },
2910
+ "margin_y": {
2911
+ "default": "0px",
2912
+ "description": "Vertical offset from the anchor in pixels or percentage of\nbase height. Anchor-relative; positive moves toward the image\ncentre.\n\n**`anchor: center` exception.** Same rejection rule as the\n`image` mime_group \u2014 non-zero `margin_y` with\n`anchor: center` is rejected by the API at workflow-create\ntime as `invalid_options` (HTTP 422).\n",
2913
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
2914
+ "type": "string"
2915
+ },
2916
+ "opacity": {
2917
+ "default": 0.5,
2918
+ "description": "Overlay opacity (0 = fully transparent, 1 = fully opaque).\nClamped to range.\n",
2919
+ "max": 1.0,
2920
+ "min": 0.0,
2921
+ "type": "float"
2922
+ },
2923
+ "overlay_width": {
2924
+ "description": "Optional overlay width as pixels or percentage of base\nwidth. Aspect ratio preserved. Omit to use the original\noverlay size.\n",
2925
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
2926
+ "type": "string"
2927
+ }
2928
+ }
2929
+ },
2869
2930
  "image_gif": {
2870
2931
  "availability": "planned",
2871
2932
  "mimes": [
@@ -2913,6 +2974,54 @@
2913
2974
  "type": "string"
2914
2975
  }
2915
2976
  }
2977
+ },
2978
+ "image_tiff": {
2979
+ "availability": "planned",
2980
+ "mimes": [
2981
+ "image/tiff"
2982
+ ],
2983
+ "options": {
2984
+ "anchor": {
2985
+ "default": "bottom_right",
2986
+ "description": "9-grid anchor position on the base image. Combined with\nmargin_x and margin_y for fine-grained offset from the\nanchor point.\n",
2987
+ "type": "enum",
2988
+ "values": [
2989
+ "top_left",
2990
+ "top_center",
2991
+ "top_right",
2992
+ "center_left",
2993
+ "center",
2994
+ "center_right",
2995
+ "bottom_left",
2996
+ "bottom_center",
2997
+ "bottom_right"
2998
+ ]
2999
+ },
3000
+ "margin_x": {
3001
+ "default": "0px",
3002
+ "description": "Horizontal offset from the anchor in pixels or percentage of\nbase width. Anchor-relative; positive moves toward the image\ncentre.\n\n**`anchor: center` exception.** Same rejection rule as the\n`image` mime_group \u2014 non-zero `margin_x` with\n`anchor: center` is rejected by the API at workflow-create\ntime as `invalid_options` (HTTP 422).\n",
3003
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
3004
+ "type": "string"
3005
+ },
3006
+ "margin_y": {
3007
+ "default": "0px",
3008
+ "description": "Vertical offset from the anchor in pixels or percentage of\nbase height. Anchor-relative; positive moves toward the image\ncentre.\n\n**`anchor: center` exception.** Same rejection rule as the\n`image` mime_group \u2014 non-zero `margin_y` with\n`anchor: center` is rejected by the API at workflow-create\ntime as `invalid_options` (HTTP 422).\n",
3009
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
3010
+ "type": "string"
3011
+ },
3012
+ "opacity": {
3013
+ "default": 0.5,
3014
+ "description": "Overlay opacity (0 = fully transparent, 1 = fully opaque).\nClamped to range.\n",
3015
+ "max": 1.0,
3016
+ "min": 0.0,
3017
+ "type": "float"
3018
+ },
3019
+ "overlay_width": {
3020
+ "description": "Optional overlay width as pixels or percentage of base\nwidth. Aspect ratio preserved. Omit to use the original\noverlay size.\n",
3021
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
3022
+ "type": "string"
3023
+ }
3024
+ }
2916
3025
  }
2917
3026
  },
2918
3027
  "min_inputs": 2,
@@ -3008,12 +3117,12 @@
3008
3117
  ]
3009
3118
  },
3010
3119
  "trim_end": {
3011
- "description": "Trim from end in seconds (per-input). Applied to this input\nBEFORE any transition/crossfade. Matches the `compress` trim\nsemantic. No `max` \u2014 the upper bound is the input's own\nduration, enforced by the worker, not statically bounded\nhere.\n",
3120
+ "description": "Trim from end in seconds (per-input). Applied to this input\nBEFORE any transition/crossfade. Matches the `compress` trim\nsemantic. No `max` \u2014 the upper bound is the input's own\nduration, enforced at processing time, not statically bounded\nhere.\n",
3012
3121
  "min": 0,
3013
3122
  "type": "float"
3014
3123
  },
3015
3124
  "trim_start": {
3016
- "description": "Trim from beginning in seconds (per-input). Applied to this\ninput BEFORE any transition/crossfade. Matches the\n`compress` trim semantic. No `max` \u2014 the upper bound is the\ninput's own duration, enforced by the worker, not statically\nbounded here.\n",
3125
+ "description": "Trim from beginning in seconds (per-input). Applied to this\ninput BEFORE any transition/crossfade. Matches the\n`compress` trim semantic. No `max` \u2014 the upper bound is the\ninput's own duration, enforced at processing time, not statically\nbounded here.\n",
3017
3126
  "min": 0,
3018
3127
  "type": "float"
3019
3128
  }
@@ -3070,7 +3179,7 @@
3070
3179
  "type": "integer"
3071
3180
  },
3072
3181
  "output_type": {
3073
- "description": "Result format: animated GIF or slideshow video. No default \u2014 caller must pick explicitly (image/collage and document/PDF concat are not supported by the V1 merge Lambda).",
3182
+ "description": "Result format: animated GIF or slideshow video. No default \u2014 caller must pick explicitly (image/collage and document/PDF concat are not supported by V1 merge).",
3074
3183
  "required": true,
3075
3184
  "type": "enum",
3076
3185
  "values": [
@@ -3637,12 +3746,12 @@
3637
3746
  ]
3638
3747
  },
3639
3748
  "trim_end": {
3640
- "description": "Trim from end in seconds (per-input). Applied to this input\nBEFORE any transition/crossfade. Matches the `compress` trim\nsemantic. No `max` \u2014 the upper bound is the input's own\nduration, enforced by the worker, not statically bounded\nhere.\n",
3749
+ "description": "Trim from end in seconds (per-input). Applied to this input\nBEFORE any transition/crossfade. Matches the `compress` trim\nsemantic. No `max` \u2014 the upper bound is the input's own\nduration, enforced at processing time, not statically bounded\nhere.\n",
3641
3750
  "min": 0,
3642
3751
  "type": "float"
3643
3752
  },
3644
3753
  "trim_start": {
3645
- "description": "Trim from beginning in seconds (per-input). Applied to this\ninput BEFORE any transition/crossfade. Matches the\n`compress` trim semantic. No `max` \u2014 the upper bound is the\ninput's own duration, enforced by the worker, not statically\nbounded here.\n",
3754
+ "description": "Trim from beginning in seconds (per-input). Applied to this\ninput BEFORE any transition/crossfade. Matches the\n`compress` trim semantic. No `max` \u2014 the upper bound is the\ninput's own duration, enforced at processing time, not statically\nbounded here.\n",
3646
3755
  "min": 0,
3647
3756
  "type": "float"
3648
3757
  }
@@ -3677,7 +3786,7 @@
3677
3786
  "passthrough": {
3678
3787
  "availability": "beta",
3679
3788
  "default": false,
3680
- "description": "Inert lossless source operation. A single-input source job whose\nSOLE operation is `passthrough` emits its source bytes UNCHANGED \u2014\nno compression, no transformation, no Lambda dispatch. The API\nself-completes the job at workflow-create/publish time: the job's\nterminal output IS the upload's `{bucket, key}` unchanged.\n\n**Purpose.** Lets an uploaded file enter a multi-input operation\n(`merge` / `archive` / `image_watermark` / etc.) LOSSLESSLY. Since\nV2 narrows `JobInputV2.source` to exclude upload-direct (`MultiInputSource`\n\u2014 job_output / external_import / connection only), an upload that\nmust feed a multi-input op enters via a `passthrough` source job\nreferenced downstream by `{ type: job_output, from: <id> }`. This\npreserves billing / DAG / lineage that direct upload-in-inputs[]\nwould bypass.\n\n**Distinct from `operations: []`.** An empty `operations[]` on a\nsingle-input upload job KEEPS its implicit-compress meaning (see\n`POST /api/workflows` description). `passthrough` is the EXPLICIT\nlossless opt-out chosen via the existing rule \"non-empty\n`operations[]` without `compress` = compression opt-out\" \u2014 it is a\nsole, named operation, not an empty chain.\n\n**Never published to SNS.** Because the API self-completes the job\nat publish, `passthrough` is INTENTIONALLY absent from the AsyncAPI\nrouting enums (`OperationType`, `OperationRequestAttributes.operation_type`)\nand has no `ops-passthrough` queue. It never appears on the wire as\na request message.\n\nMedia-agnostic (no `mime_groups`): it accepts any uploaded MIME and\ncopies it verbatim, so there are no per-MIME options or constraints.\n`availability: beta` \u2014 the API self-completion path is LIVE (the\ninputs[]-narrowing + passthrough self-complete mechanism is deployed),\nso workflow-create accepts `passthrough` source jobs and MUST NOT\nreturn `feature_not_available`. Activated per ticket\n[`4som89Uh`](https://trello.com/c/4som89Uh) + ADR-0004 (planned\u2192beta\nflip; self-complete code deployed API-side).\n",
3789
+ "description": "Inert lossless source operation. A single-input source job whose\nSOLE operation is `passthrough` emits its source bytes UNCHANGED \u2014\nno compression, no transformation, no backend dispatch. The API\nself-completes the job at workflow-create/publish time: the job's\nterminal output IS the upload's `{bucket, key}` unchanged.\n\n**Purpose.** Lets an uploaded file enter a multi-input operation\n(`merge` / `archive` / `image_watermark` / etc.) LOSSLESSLY. Since\nV2 narrows the input source to exclude upload-direct (a multi-input source\n\u2014 job_output / external_import / connection only), an upload that\nmust feed a multi-input op enters via a `passthrough` source job\nreferenced downstream by `{ type: job_output, from: <id> }`. This\npreserves billing / DAG / lineage that direct upload-in-inputs[]\nwould bypass.\n\n**Distinct from `operations: []`.** An empty `operations[]` on a\nsingle-input upload job KEEPS its implicit-compress meaning (see\n`POST /api/workflows` description). `passthrough` is the EXPLICIT\nlossless opt-out chosen via the existing rule \"non-empty\n`operations[]` without `compress` = compression opt-out\" \u2014 it is a\nsole, named operation, not an empty chain.\n\n**Never published to SNS.** Because the API self-completes the job\nat publish, `passthrough` is INTENTIONALLY absent from the AsyncAPI\nrouting enums (`OperationType`, `OperationRequestAttributes.operation_type`)\nand has no `ops-passthrough` queue. It never appears on the wire as\na request message.\n\nMedia-agnostic (no `mime_groups`): it accepts any uploaded MIME and\ncopies it verbatim, so there are no per-MIME options or constraints.\n`availability: beta` \u2014 the API self-completion path is LIVE (the\ninputs[]-narrowing + passthrough self-complete mechanism is deployed),\nso workflow-create accepts `passthrough` source jobs and MUST NOT\nreturn `feature_not_available`. Activated per ticket\n[`4som89Uh`](https://trello.com/c/4som89Uh) (planned\u2192beta flip;\nself-complete code deployed API-side).\n",
3681
3790
  "input_model": "single"
3682
3791
  },
3683
3792
  "render_variants": {
@@ -3771,11 +3880,11 @@
3771
3880
  "split": {
3772
3881
  "availability": "beta",
3773
3882
  "default": false,
3774
- "description": "Fan one input file into N outputs across GIF, PDF, audio, and\nvideo MIME families. Single-input \u2014 one input file per job, fanned\ninto N outputs per the per-mime-group catalog. Mirrors the\n`merge` / `convert` catalog-split-by-mime-group pattern.\n\n**Three shared modes** (audio + video; the mode discriminator\ngates which option applies per the `depends_on` rules in this\nschema):\n- `interval`: split every N numeric-seconds.\n- `count`: split into N equal-duration pieces (integer 2..=200).\n- `cut_points`: explicit cut points in numeric-seconds (strictly\n increasing, no duplicates, each > 0 and < probed duration).\n\n**Plus an audio-only `silence` mode** (`beta`, gated by the\n`silence_mode_audio` feature): cut at detected silence gaps via\nthe worker's ffmpeg `silencedetect` filter. Video stays\nthree-mode.\n\n**GIF + PDF use range-based selection** instead of three modes:\n- GIF (`image_gif`): `frame_range` REQUIRED + `output_format`\n enum {png,webp,jpg}.\n- PDF (`document_pdf`): `page_range` OR `page_groups` (mutually\n exclusive per `depends_on`).\n\n**Wire format**: numeric seconds (floats allowed for sub-second\nprecision). NOT ISO 8601 duration strings. Matches FFmpeg /\nCloudinary / Shotstack / AWS MediaConvert conventions.\n\n**200-output hard cap** per ADR-0009 \u00a7D5\n(`OperationResult.outputs[].maxItems: 200`). Preflight rejects\nrequests exceeding this BEFORE work as `invalid_options` (422).\nCap math by mode:\n- `interval`: `ceil(probed_duration / interval) <= 200`\n- `count`: enforced by the option's `min: 2 max: 200` range\n- `cut_points`: `cut_points.len() + 1 <= 200`\n- `silence` (audio): resolved segment count (detected gaps + 1)\n `<= 200`; preflight rejects as `invalid_options` (422)\n- PDF: resolved page count after expansion `<= 200`\n (cross-ref [`WgCqnMRa`](https://trello.com/c/WgCqnMRa))\n- GIF: resolved frame count after expansion `<= 200`\n\n**Output naming**: 3-digit zero-padded \u2014 `output-001` ..\n`output-200`. Matches the convert PDF\u2192N precedent.\n\n**Output envelope binding** (per ADR-0014 + ADR-0009 \u00a7D2):\n- `image_gif`: `PositionIndexed` (frame stream-position 0-based\n ordinal)\n- `document_pdf`: `PageIndexed` (1-based gapless page index)\n- `audio`: `PositionIndexed` (cut stream-position ordinal)\n- `video`: `PositionIndexed` (cut stream-position ordinal)\n\n**`precision` flag** (audio + video only): `fast` (default) =\npacket-boundary approximate cuts; `exact` = re-encode-aligned\nprecise cuts. Mime_group scoping is the gate \u2014 precision does\nNOT apply to GIF or PDF (frame extraction is exact and PDF page\nselection is exact by definition).\n\n**Long-form video** routes to a separate `split-video-fargate`\nLambda (same `split` OperationType, different worker; routing\nvia `processing_class`). No schema-side discriminator beyond MIME\ndetection + `processing_class`.\n\n`availability: beta` for the `audio` and `video` mime_groups\n(workers live on staging \u2014 shape-stable + opt-in, MUST NOT 422).\nVideo activates **both classes**: `video.processing_class.short_form`\nAND `long_form` are `beta` \u2014 the `split-video-fargate` worker is\ndeployed + wired on staging but NOT yet proven end-to-end\n([`rcwvUKhI`](https://trello.com/c/rcwvUKhI); the first customer-path\nsoak has not completed and the 4GB+ speed-up is unmeasured; the\nlong-form fan-out is flag-gated dark). The `image_gif` and\n`document_pdf` mime_groups stay `availability: planned` until their\ncross-repo Lambda workers ship ([`vKI0CFDu`](https://trello.com/c/vKI0CFDu)\n+ lambdas L1); dispatch returns `feature_not_available` (422) for the\nstill-planned groups/classes until then.\n\nPer ADR-0014.\n",
3883
+ "description": "Fan one input file into N outputs across GIF, PDF, audio, and\nvideo MIME families. Single-input \u2014 one input file per job, fanned\ninto N outputs per the per-mime-group catalog. Mirrors the\n`merge` / `convert` catalog-split-by-mime-group pattern.\n\n**Three shared modes** (audio + video; the mode discriminator\ngates which option applies per the `depends_on` rules in this\nschema):\n- `interval`: split every N numeric-seconds.\n- `count`: split into N equal-duration pieces (integer 2..=200).\n- `cut_points`: explicit cut points in numeric-seconds (strictly\n increasing, no duplicates, each > 0 and < probed duration).\n\n**Plus an audio-only `silence` mode** (`beta`, gated by the\n`silence_mode_audio` feature): cut at detected silence gaps via\nserver-side silence detection. Video stays three-mode.\n\n**GIF + PDF use range-based selection** instead of three modes:\n- GIF (`image_gif`): `frame_range` REQUIRED + `output_format`\n enum {png,webp,jpg}.\n- PDF (`document_pdf`): `page_range` OR `page_groups` (mutually\n exclusive per `depends_on`).\n\n**Wire format**: numeric seconds (floats allowed for sub-second\nprecision). NOT ISO 8601 duration strings. Matches common\nvideo-tooling conventions.\n\n**200-output hard cap** (`OperationResult.outputs[].maxItems:\n200`). Preflight rejects\nrequests exceeding this BEFORE work as `invalid_options` (422).\nCap math by mode:\n- `interval`: `ceil(probed_duration / interval) <= 200`\n- `count`: enforced by the option's `min: 2 max: 200` range\n- `cut_points`: `cut_points.len() + 1 <= 200`\n- `silence` (audio): resolved segment count (detected gaps + 1)\n `<= 200`; preflight rejects as `invalid_options` (422)\n- PDF: resolved page count after expansion `<= 200`\n (cross-ref [`WgCqnMRa`](https://trello.com/c/WgCqnMRa))\n- GIF: resolved frame count after expansion `<= 200`\n\n**Output naming**: 3-digit zero-padded \u2014 `output-001` ..\n`output-200`. Matches the convert PDF\u2192N precedent.\n\n**Output envelope binding**:\n- `image_gif`: `PositionIndexed` (frame stream-position 0-based\n ordinal)\n- `document_pdf`: `PageIndexed` (1-based gapless page index)\n- `audio`: `PositionIndexed` (cut stream-position ordinal)\n- `video`: `PositionIndexed` (cut stream-position ordinal)\n\n**`precision` flag** (audio + video only): `fast` (default) =\npacket-boundary approximate cuts; `exact` = re-encode-aligned\nprecise cuts. Mime_group scoping is the gate \u2014 precision does\nNOT apply to GIF or PDF (frame extraction is exact and PDF page\nselection is exact by definition).\n\n**Long-form video** routes to a separate long-form processing\npath (same `split` OperationType; routing via `processing_class`).\nNo schema-side discriminator beyond MIME detection +\n`processing_class`.\n\n`availability: beta` for the `audio` and `video` mime_groups\n(shape-stable + opt-in, MUST NOT 422). Video activates **both\nclasses**: `video.processing_class.short_form` AND `long_form` are\n`beta`. The `image_gif` and `document_pdf` mime_groups stay\n`availability: planned` until their cross-repo backend support ships;\ndispatch returns `feature_not_available` (422) for the still-planned\ngroups/classes until then.\n",
3775
3884
  "features": {
3776
3885
  "silence_mode_audio": {
3777
3886
  "availability": "beta",
3778
- "description": "Silence-detect cut mode for audio (`mode: silence` \u2014\nserver-side silence detection in the audio split worker).\n`beta`: shape-stable + opt-in. The worker is built; the mode\nships dark until the API regenerates enum validation to accept\n`mode: silence`. The `silence` enum value carries a matching\n`per_value_availability: beta` on `audio.mode`.\n"
3887
+ "description": "Silence-detect cut mode for audio (`mode: silence` \u2014\nserver-side silence detection). `beta`: shape-stable + opt-in.\nThe mode ships dark until the API regenerates enum validation\nto accept `mode: silence`. The `silence` enum value carries a\nmatching `per_value_availability: beta` on `audio.mode`.\n"
3779
3888
  }
3780
3889
  },
3781
3890
  "input_model": "single",
@@ -3843,7 +3952,7 @@
3843
3952
  },
3844
3953
  "precision": {
3845
3954
  "default": "fast",
3846
- "description": "Cut precision. Under `fast`, audio cuts are\npacket-boundary approximate (drift bounded by codec\npacket size \u2014 typically <= 23ms for AAC, <= 26ms for\nMP3). Under `exact`, the Lambda re-encodes to align cuts\nprecisely at the requested timestamps (slower, larger\noutput, but sample-accurate).\n",
3955
+ "description": "Cut precision. Under `fast`, audio cuts are\npacket-boundary approximate (drift bounded by codec\npacket size \u2014 typically <= 23ms for AAC, <= 26ms for\nMP3). Under `exact`, the server re-encodes to align cuts\nprecisely at the requested timestamps (slower, larger\noutput, but sample-accurate).\n",
3847
3956
  "type": "enum",
3848
3957
  "values": [
3849
3958
  "fast",
@@ -3994,7 +4103,7 @@
3994
4103
  },
3995
4104
  "precision": {
3996
4105
  "default": "fast",
3997
- "description": "Cut precision. Under `fast`, video cuts are keyframe-\naligned (drift bounded by GOP size \u2014 typically up to 1-2\nseconds depending on encoder). Under `exact`, the Lambda\nre-encodes to align cuts precisely at the requested\ntimestamps (slower, larger output, but frame-accurate).\n",
4106
+ "description": "Cut precision. Under `fast`, video cuts are keyframe-\naligned (drift bounded by GOP size \u2014 typically up to 1-2\nseconds depending on encoder). Under `exact`, the backend\nre-encodes to align cuts precisely at the requested\ntimestamps (slower, larger output, but frame-accurate).\n",
3998
4107
  "type": "enum",
3999
4108
  "values": [
4000
4109
  "fast",
@@ -4033,7 +4142,7 @@
4033
4142
  },
4034
4143
  "text_watermark": {
4035
4144
  "default": false,
4036
- "description": "Render a text overlay onto an image using bundled Liberation Sans\n(SIL OFL). Single-input \u2014 text comes from options, not from a file\ninput. Source capped at 100 megapixels in `single` mode (raised from\n25MP \u2014 lambdas #250, proven on the 45.9MP report case). `tiled` mode\nkeeps the 25-megapixel canvas bound (see `tile_spacing` /\n`watermark_mode`). Per ADR-0004 \u00a7\"V2 JobDefinition\" + plan v5 \u00a7F1.\n\nTwo rendering modes:\n- `single`: one label rendered at the anchor + margin position.\n- `tiled`: text tiled across the source image (Cinavia / Adobe Stock\n pattern); rotation default -45\u00b0 and tile_spacing controls density.\n",
4145
+ "description": "Render a text overlay onto an image using bundled Liberation Sans\n(SIL OFL). Single-input \u2014 text comes from options, not from a file\ninput. Source capped at 100 megapixels in `single` mode. `tiled`\nmode keeps the 25-megapixel canvas bound (see `tile_spacing` /\n`watermark_mode`).\n\nTwo rendering modes:\n- `single`: one label rendered at the anchor + margin position.\n- `tiled`: text tiled across the source image (Cinavia / Adobe Stock\n pattern); rotation default -45\u00b0 and tile_spacing controls density.\n\nStable today for image/jpeg, image/png, image/webp inputs. TIFF\n(image/tiff) and BMP (image/bmp) inputs are advertised as `planned`\nvia the parallel `image_tiff` / `image_bmp` mime_groups \u2014 both are\nalready decodable and the worker's encode arms are a cheap add\n(shared `image`-crate encoders), so each flips to `stable` once its\narm ships and is proven. Output mirrors the input format\n(`produces: same_as_input`), so a format is only advertised when the\nworker can both decode AND encode it; HEIC/AVIF (no encoder) and SVG\n(cannot re-emit svg from a rasterised composite) stay out of scope.\n",
4037
4146
  "input_model": "single",
4038
4147
  "mime_groups": {
4039
4148
  "image": {
@@ -4130,6 +4239,194 @@
4130
4239
  ]
4131
4240
  }
4132
4241
  }
4242
+ },
4243
+ "image_bmp": {
4244
+ "availability": "planned",
4245
+ "mimes": [
4246
+ "image/bmp"
4247
+ ],
4248
+ "options": {
4249
+ "anchor": {
4250
+ "default": "bottom_right",
4251
+ "description": "9-grid anchor position. For watermark_mode: single, the text\nlabel is placed at this anchor + margin_x/margin_y offset.\nFor watermark_mode: tiled, anchor is ignored (tiles fill the\nsource image).\n",
4252
+ "type": "enum",
4253
+ "values": [
4254
+ "top_left",
4255
+ "top_center",
4256
+ "top_right",
4257
+ "center_left",
4258
+ "center",
4259
+ "center_right",
4260
+ "bottom_left",
4261
+ "bottom_center",
4262
+ "bottom_right"
4263
+ ]
4264
+ },
4265
+ "color": {
4266
+ "default": "#FFFFFF80",
4267
+ "description": "Text colour as hex RGB (#RRGGBB) or RGBA (#RRGGBBAA).\nDefault is white with 50% alpha.\n",
4268
+ "pattern": "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$",
4269
+ "type": "string"
4270
+ },
4271
+ "font_family": {
4272
+ "default": "liberation_sans",
4273
+ "description": "Font family. V2.0 ships with bundled Liberation Sans only;\nuser-uploaded fonts are a future extension (separate ticket).\n",
4274
+ "type": "enum",
4275
+ "values": [
4276
+ "liberation_sans"
4277
+ ]
4278
+ },
4279
+ "font_size": {
4280
+ "default": 48.0,
4281
+ "description": "Font size in pixels. Clamped to range.",
4282
+ "max": 512.0,
4283
+ "min": 8.0,
4284
+ "type": "float"
4285
+ },
4286
+ "margin_x": {
4287
+ "default": "0px",
4288
+ "description": "Horizontal offset from the anchor as pixels (e.g. \"40px\") or\npercentage of base width (e.g. \"5%\"). Direction is\nanchor-relative. Ignored when watermark_mode: tiled.\n\n**`anchor: center` exception** (watermark_mode: single only).\nNon-zero `margin_x` combined with `anchor: center` is rejected\nby the API at workflow-create time as `invalid_options`\n(HTTP 422). Not applicable to watermark_mode: tiled, where\nanchor + margin are ignored entirely.\n",
4289
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
4290
+ "type": "string"
4291
+ },
4292
+ "margin_y": {
4293
+ "default": "0px",
4294
+ "description": "Vertical offset from the anchor as pixels or percentage of\nbase height. Ignored when watermark_mode: tiled.\n\n**`anchor: center` exception** (watermark_mode: single only).\nSame rejection rule as `margin_x` \u2014 non-zero `margin_y` with\n`anchor: center` is rejected by the API at workflow-create\ntime as `invalid_options` (HTTP 422).\n",
4295
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
4296
+ "type": "string"
4297
+ },
4298
+ "opacity": {
4299
+ "default": 1.0,
4300
+ "description": "Text overlay opacity. Default 1.0 \u2014 the alpha channel of\n`color` already encodes transparency, so opacity is usually\nleft at 1.0 and transparency is set via the color hex.\n",
4301
+ "max": 1.0,
4302
+ "min": 0.0,
4303
+ "type": "float"
4304
+ },
4305
+ "rotation": {
4306
+ "default": -45.0,
4307
+ "description": "Rotation angle in degrees. Default -45\u00b0 suits tiled mode\n(angled diagonal pattern). Clamped to range.\n",
4308
+ "max": 360.0,
4309
+ "min": -360.0,
4310
+ "type": "float"
4311
+ },
4312
+ "text": {
4313
+ "description": "Watermark text. Must be non-empty.\n",
4314
+ "required": true,
4315
+ "type": "string"
4316
+ },
4317
+ "tile_spacing": {
4318
+ "depends_on": {
4319
+ "watermark_mode": "tiled"
4320
+ },
4321
+ "description": "Spacing between tiled labels in pixels. Defaults to font_size\nwhen omitted. Only applies when watermark_mode is tiled.\n",
4322
+ "max": 1000,
4323
+ "min": 0,
4324
+ "type": "integer"
4325
+ },
4326
+ "watermark_mode": {
4327
+ "default": "single",
4328
+ "description": "Rendering mode:\n- `single`: one label placed at the anchor + margin offset.\n- `tiled`: repeated tiles across the entire source image at\n the rotation angle.\n",
4329
+ "type": "enum",
4330
+ "values": [
4331
+ "single",
4332
+ "tiled"
4333
+ ]
4334
+ }
4335
+ }
4336
+ },
4337
+ "image_tiff": {
4338
+ "availability": "planned",
4339
+ "mimes": [
4340
+ "image/tiff"
4341
+ ],
4342
+ "options": {
4343
+ "anchor": {
4344
+ "default": "bottom_right",
4345
+ "description": "9-grid anchor position. For watermark_mode: single, the text\nlabel is placed at this anchor + margin_x/margin_y offset.\nFor watermark_mode: tiled, anchor is ignored (tiles fill the\nsource image).\n",
4346
+ "type": "enum",
4347
+ "values": [
4348
+ "top_left",
4349
+ "top_center",
4350
+ "top_right",
4351
+ "center_left",
4352
+ "center",
4353
+ "center_right",
4354
+ "bottom_left",
4355
+ "bottom_center",
4356
+ "bottom_right"
4357
+ ]
4358
+ },
4359
+ "color": {
4360
+ "default": "#FFFFFF80",
4361
+ "description": "Text colour as hex RGB (#RRGGBB) or RGBA (#RRGGBBAA).\nDefault is white with 50% alpha.\n",
4362
+ "pattern": "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$",
4363
+ "type": "string"
4364
+ },
4365
+ "font_family": {
4366
+ "default": "liberation_sans",
4367
+ "description": "Font family. V2.0 ships with bundled Liberation Sans only;\nuser-uploaded fonts are a future extension (separate ticket).\n",
4368
+ "type": "enum",
4369
+ "values": [
4370
+ "liberation_sans"
4371
+ ]
4372
+ },
4373
+ "font_size": {
4374
+ "default": 48.0,
4375
+ "description": "Font size in pixels. Clamped to range.",
4376
+ "max": 512.0,
4377
+ "min": 8.0,
4378
+ "type": "float"
4379
+ },
4380
+ "margin_x": {
4381
+ "default": "0px",
4382
+ "description": "Horizontal offset from the anchor as pixels (e.g. \"40px\") or\npercentage of base width (e.g. \"5%\"). Direction is\nanchor-relative. Ignored when watermark_mode: tiled.\n\n**`anchor: center` exception** (watermark_mode: single only).\nNon-zero `margin_x` combined with `anchor: center` is rejected\nby the API at workflow-create time as `invalid_options`\n(HTTP 422). Not applicable to watermark_mode: tiled, where\nanchor + margin are ignored entirely.\n",
4383
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
4384
+ "type": "string"
4385
+ },
4386
+ "margin_y": {
4387
+ "default": "0px",
4388
+ "description": "Vertical offset from the anchor as pixels or percentage of\nbase height. Ignored when watermark_mode: tiled.\n\n**`anchor: center` exception** (watermark_mode: single only).\nSame rejection rule as `margin_x` \u2014 non-zero `margin_y` with\n`anchor: center` is rejected by the API at workflow-create\ntime as `invalid_options` (HTTP 422).\n",
4389
+ "pattern": "^\\d+(\\.\\d+)?(px|%)$",
4390
+ "type": "string"
4391
+ },
4392
+ "opacity": {
4393
+ "default": 1.0,
4394
+ "description": "Text overlay opacity. Default 1.0 \u2014 the alpha channel of\n`color` already encodes transparency, so opacity is usually\nleft at 1.0 and transparency is set via the color hex.\n",
4395
+ "max": 1.0,
4396
+ "min": 0.0,
4397
+ "type": "float"
4398
+ },
4399
+ "rotation": {
4400
+ "default": -45.0,
4401
+ "description": "Rotation angle in degrees. Default -45\u00b0 suits tiled mode\n(angled diagonal pattern). Clamped to range.\n",
4402
+ "max": 360.0,
4403
+ "min": -360.0,
4404
+ "type": "float"
4405
+ },
4406
+ "text": {
4407
+ "description": "Watermark text. Must be non-empty.\n",
4408
+ "required": true,
4409
+ "type": "string"
4410
+ },
4411
+ "tile_spacing": {
4412
+ "depends_on": {
4413
+ "watermark_mode": "tiled"
4414
+ },
4415
+ "description": "Spacing between tiled labels in pixels. Defaults to font_size\nwhen omitted. Only applies when watermark_mode is tiled.\n",
4416
+ "max": 1000,
4417
+ "min": 0,
4418
+ "type": "integer"
4419
+ },
4420
+ "watermark_mode": {
4421
+ "default": "single",
4422
+ "description": "Rendering mode:\n- `single`: one label placed at the anchor + margin offset.\n- `tiled`: repeated tiles across the entire source image at\n the rotation angle.\n",
4423
+ "type": "enum",
4424
+ "values": [
4425
+ "single",
4426
+ "tiled"
4427
+ ]
4428
+ }
4429
+ }
4133
4430
  }
4134
4431
  },
4135
4432
  "sole_op": true
@@ -4184,7 +4481,7 @@
4184
4481
  "depends_on": {
4185
4482
  "source": "page"
4186
4483
  },
4187
- "description": "1-based index of the printed page to render, after print-layout pagination is applied. For office formats this is the page in the LibreOffice PDF export \u2014 NOT a 1:1 sheet/slide index: a spreadsheet sheet may span multiple printed pages and multi-sheet workbooks paginate their sheets in order. For PDF inputs it is the PDF page number directly.",
4484
+ "description": "1-based index of the printed page to render, after print-layout pagination is applied. For office formats this is the page in the document's print layout \u2014 NOT a 1:1 sheet/slide index: a spreadsheet sheet may span multiple printed pages and multi-sheet workbooks paginate their sheets in order. For PDF inputs it is the PDF page number directly.",
4188
4485
  "min": 1,
4189
4486
  "type": "integer"
4190
4487
  },
@@ -4232,6 +4529,14 @@
4232
4529
  "image/heic"
4233
4530
  ],
4234
4531
  "options": {
4532
+ "background": {
4533
+ "depends_on": {
4534
+ "format": "jpg"
4535
+ },
4536
+ "description": "Background colour as a 6-digit hex (`#RRGGBB`, e.g. `#ffffff`) for a transparent image (e.g. PNG/WebP with alpha) resized to an opaque JPG thumbnail \u2014 the alpha-flatten fill colour. Mirrors convert.image.background. HEX ONLY: a non-hex value (e.g. a CSS named colour) is rejected rather than advertising an inert free-form string. When omitted, transparency is flattened onto white. JPG output only: PNG/WebP keep alpha, so the flatten (and this option) do not apply.",
4537
+ "pattern": "^#[0-9a-fA-F]{6}$",
4538
+ "type": "string"
4539
+ },
4235
4540
  "fit": {
4236
4541
  "default": "crop",
4237
4542
  "description": "Resize mode",
@@ -4289,6 +4594,70 @@
4289
4594
  }
4290
4595
  }
4291
4596
  },
4597
+ "image_svg": {
4598
+ "availability": "planned",
4599
+ "max_output_pixels": 16000000,
4600
+ "mimes": [
4601
+ "image/svg+xml"
4602
+ ],
4603
+ "options": {
4604
+ "background": {
4605
+ "depends_on": {
4606
+ "format": "jpg"
4607
+ },
4608
+ "description": "Background colour as a 6-digit hex (`#RRGGBB`, e.g. `#ffffff`) for a transparent image (e.g. PNG/WebP with alpha) resized to an opaque JPG thumbnail \u2014 the alpha-flatten fill colour. Mirrors convert.image.background. HEX ONLY: a non-hex value (e.g. a CSS named colour) is rejected rather than advertising an inert free-form string. When omitted, transparency is flattened onto white. JPG output only: PNG/WebP keep alpha, so the flatten (and this option) do not apply.",
4609
+ "pattern": "^#[0-9a-fA-F]{6}$",
4610
+ "type": "string"
4611
+ },
4612
+ "fit": {
4613
+ "default": "crop",
4614
+ "description": "Resize mode",
4615
+ "type": "enum",
4616
+ "values": [
4617
+ "max",
4618
+ "crop",
4619
+ "scale"
4620
+ ]
4621
+ },
4622
+ "format": {
4623
+ "default": "jpg",
4624
+ "description": "Output format for the thumbnail",
4625
+ "type": "enum",
4626
+ "values": [
4627
+ "jpg",
4628
+ "png",
4629
+ "webp"
4630
+ ]
4631
+ },
4632
+ "height": {
4633
+ "description": "Target height in pixels (1-16384; width*height <= 16MP). For image input this is a full resize, not just a small preview.",
4634
+ "max": 16384,
4635
+ "min": 1,
4636
+ "required": true,
4637
+ "type": "integer"
4638
+ },
4639
+ "quality": {
4640
+ "default": 85,
4641
+ "depends_on": {
4642
+ "format": [
4643
+ "jpg",
4644
+ "webp"
4645
+ ]
4646
+ },
4647
+ "description": "Output quality for lossy thumbnail formats (jpg/webp): higher = better quality, larger file. Ignored for png (lossless).",
4648
+ "max": 100,
4649
+ "min": 1,
4650
+ "type": "integer"
4651
+ },
4652
+ "width": {
4653
+ "description": "Target width in pixels (1-16384; width*height <= 16MP). For image input this is a full resize, not just a small preview.",
4654
+ "max": 16384,
4655
+ "min": 1,
4656
+ "required": true,
4657
+ "type": "integer"
4658
+ }
4659
+ }
4660
+ },
4292
4661
  "video": {
4293
4662
  "max_output_pixels": 16000000,
4294
4663
  "mimes": [
@@ -4318,7 +4687,7 @@
4318
4687
  ]
4319
4688
  },
4320
4689
  "height": {
4321
- "description": "Target height in pixels (1-16384; width*height <= 16MP). Rounded down to an even number by the worker.",
4690
+ "description": "Target height in pixels (1-16384; width*height <= 16MP). Rounded down to an even number.",
4322
4691
  "max": 16384,
4323
4692
  "min": 1,
4324
4693
  "required": true,
@@ -4343,7 +4712,7 @@
4343
4712
  "type": "string"
4344
4713
  },
4345
4714
  "width": {
4346
- "description": "Target width in pixels (1-16384; width*height <= 16MP). Rounded down to an even number by the worker.",
4715
+ "description": "Target width in pixels (1-16384; width*height <= 16MP). Rounded down to an even number.",
4347
4716
  "max": 16384,
4348
4717
  "min": 1,
4349
4718
  "required": true,
@@ -4378,7 +4747,7 @@
4378
4747
  "video_text_watermark": {
4379
4748
  "availability": "planned",
4380
4749
  "default": false,
4381
- "description": "Render a text overlay onto a base video using bundled Liberation\nSans (SIL OFL) via FFmpeg's `drawtext` filter. Single-input \u2014 the\ntext and its styling come from options, not from a file input.\nPer ADR-0013.\n\n**Dedicated operation, not an extension of `text_watermark`.**\n`text_watermark` renders onto images (single-pass Rust path);\n`video_text_watermark` renders per-frame via FFmpeg drawtext +\nre-encode (overlay = pixel modification, no stream-copy escape).\nDifferent runtime, different deploy unit.\n\n**Audio passthrough.** The base video's audio stream is preserved\nunchanged via FFmpeg stream-copy \u2014 the watermark applies to the\nvisual track only.\n\nTwo rendering modes (mirrors `text_watermark`):\n- `single`: one label rendered at the anchor + margin position.\n- `tiled`: text tiled across the source frames at the rotation\n angle.\n\n`availability: planned` until the cross-repo Lambda support ships;\ndispatch returns `feature_not_available` (422) until then. Per\nADR-0013 + lambdas pre-launch epic Wave A (ticket\n[`4NrRPCgh`](https://trello.com/c/4NrRPCgh)).\n",
4750
+ "description": "Render a text overlay onto a base video using bundled Liberation\nSans (SIL OFL). Single-input \u2014 the text and its styling come from\noptions, not from a file input.\n\n**Dedicated operation, not an extension of `text_watermark`.**\n`text_watermark` renders onto images (single-pass); per-frame video\ntext rendering requires a re-encode (overlay = pixel modification,\nno stream-copy escape). Different runtime.\n\n**Audio passthrough.** The base video's audio stream is preserved\nunchanged (stream-copied) \u2014 the watermark applies to the visual\ntrack only.\n\nTwo rendering modes (mirrors `text_watermark`):\n- `single`: one label rendered at the anchor + margin position.\n- `tiled`: text tiled across the source frames at the rotation\n angle.\n\n`availability: planned` until the cross-repo backend support ships;\ndispatch returns `feature_not_available` (422) until then.\n",
4382
4751
  "input_model": "single",
4383
4752
  "mime_groups": {
4384
4753
  "video": {
@@ -4506,11 +4875,11 @@
4506
4875
  "video_watermark": {
4507
4876
  "availability": "beta",
4508
4877
  "default": false,
4509
- "description": "Apply an image overlay onto a base video\nasset using FFmpeg's `overlay` filter. Multi-input role-based:\nexactly one input with `role: base` (the source video) + exactly\none with `role: overlay` (the watermark image). Each input is a\n`MultiInputSource` \u2014 an external_import handle, a vault connection,\nor an upstream `job_output` (uploads are NOT referenced directly\ninside inputs[]). To use an uploaded base or overlay, feed it through\na `passthrough` source job and reference it via `{ type: job_output,\nfrom: <id> }` (per ticket 4som89Uh).\n\n**Dedicated operation, not an extension of `image_watermark`.**\n`image_watermark` is pure-Rust/no-FFmpeg and handles\nimage-on-image overlay only. `video_watermark` requires FFmpeg's\noverlay filter + re-encode + a different deploy unit (no\nstream-copy escape \u2014 overlay = pixel modification). Per ADR-0013.\n\n**Audio passthrough.** The base video's audio stream is preserved\nunchanged via FFmpeg stream-copy \u2014 the watermark applies to the\nvisual track only.\n\nActivated to `availability: beta` (Wave A, ticket\n[`c3uthIP4`](https://trello.com/c/c3uthIP4)): the cross-repo Lambda\nbackend is live (API accepts + publishes via the operations SNS\ntopic), so the operation and its `short_form` processing class are\nshape-stable + opt-in and MUST NOT return `feature_not_available`.\n`long_form` stays `planned` (no live Fargate worker yet \u2014 its\nlong-form path is not provisioned) and the `multi_overlay_stack`\nfeature stays `planned` (Lambda support unconfirmed); requesting\nthose planned sub-features DOES still return `feature_not_available`\nuntil they are separately activated. Per ADR-0013 + lambdas\npre-launch epic Wave A (ticket\n[`SlluxMBN`'s sibling `4NrRPCgh`](https://trello.com/c/4NrRPCgh)).\n",
4878
+ "description": "Apply an image overlay onto a base video\nasset. Multi-input role-based:\nexactly one input with `role: base` (the source video) + exactly\none with `role: overlay` (the watermark image). Each input is a\na multi-input source \u2014 an external_import handle, a vault connection,\nor an upstream `job_output` (uploads are NOT referenced directly\ninside inputs[]). To use an uploaded base or overlay, feed it through\na `passthrough` source job and reference it via `{ type: job_output,\nfrom: <id> }` (per ticket 4som89Uh).\n\n**Dedicated operation, not an extension of `image_watermark`.**\n`image_watermark` handles image-on-image overlay only.\n`video_watermark` requires an overlay + re-encode (no stream-copy escape \u2014 overlay = pixel modification).\n\n**Audio passthrough.** The base video's audio stream is preserved\nunchanged (stream-copied) \u2014 the watermark applies to the visual\ntrack only.\n\nActivated to `availability: beta` (Wave A, ticket\n[`c3uthIP4`](https://trello.com/c/c3uthIP4)): the cross-repo backend\nis live (API accepts + publishes via the operations SNS\ntopic), so the operation and its `short_form` processing class are\nshape-stable + opt-in and MUST NOT return `feature_not_available`.\n`long_form` stays `planned` (its long-form path is not yet\nprovisioned) and the `multi_overlay_stack` feature stays `planned`\n(backend support unconfirmed); requesting those planned sub-features\nDOES still return `feature_not_available` until they are separately\nactivated.\n",
4510
4879
  "features": {
4511
4880
  "multi_overlay_stack": {
4512
4881
  "availability": "planned",
4513
- "description": "Allow up to 8 overlay inputs per job (currently capped at 1\noverlay). Per-overlay placement via JobInputV2.per_input_options.\nLambda-side support not yet confirmed; tagged planned per\nADR-0001 \u00a71.4. Mirrors the same-named feature on\n`image_watermark`.\n"
4882
+ "description": "Allow up to 8 overlay inputs per job (currently capped at 1\noverlay). Per-overlay placement via per-input placement options.\nBackend support not yet confirmed; tagged planned. Mirrors\nthe same-named feature on `image_watermark`.\n"
4514
4883
  }
4515
4884
  },
4516
4885
  "input_model": "multi",
@@ -4604,7 +4973,7 @@
4604
4973
  "sole_op": true
4605
4974
  }
4606
4975
  },
4607
- "schema_version": "2.117.0",
4976
+ "schema_version": "2.123.0",
4608
4977
  "source_commit": null,
4609
4978
  "user_tier": null,
4610
4979
  "workflow_features": {