diff --git a/api.oas3.yaml b/api.oas3.yaml index 58390a8..c59aa4c 100644 --- a/api.oas3.yaml +++ b/api.oas3.yaml @@ -92,6 +92,12 @@ paths: /probe/{url}: $ref: "./paths/probe.yaml" + /generate: + $ref: "./paths/generate.yaml" + + /generate/{id}: + $ref: "./paths/generateid.yaml" + /assets/{id}: $ref: "./paths/assetsid.yaml" diff --git a/paths/generate.yaml b/paths/generate.yaml new file mode 100644 index 0000000..e33a531 --- /dev/null +++ b/paths/generate.yaml @@ -0,0 +1,55 @@ + post: + responses: + "200": + description: The generated asset was already cached and is immediately available. + content: + application/json: + schema: + $ref: "../schemas/responses/generationresponse.yaml#/GenerationResponse" + "202": + description: The generation job has been queued. Poll the status endpoint. + headers: + Location: + description: The relative URL to poll for job status. + schema: + type: string + Retry-After: + description: Suggested seconds to wait before polling. + schema: + type: integer + content: + application/json: + schema: + $ref: "../schemas/responses/generationresponse.yaml#/GenerationResponse" + description: | + Generate a single image, video or audio asset from a text prompt without + rendering a full edit. Submit a prompt-bearing asset; the response is + immediate when an identical asset has been generated before (results are + cached by prompt, model and options), otherwise the job is queued and can + be polled via the status endpoint. + + Generation is billed in credits per asset. Identical repeat requests + resolve from the cache at no charge. + + **Base URL:** https://api.shotstack.io/edit/{version} + summary: Generate Asset + operationId: postGenerate + requestBody: + description: >- + A prompt-bearing image, video or audio asset to generate. + content: + application/json: + schema: + type: object + properties: + asset: + oneOf: + - $ref: "../schemas/imageasset.yaml#/ImageAsset" + - $ref: "../schemas/videoasset.yaml#/VideoAsset" + - $ref: "../schemas/audioasset.yaml#/AudioAsset" + required: + - asset + security: + - DeveloperKey: [] + tags: + - Edit diff --git a/paths/generateid.yaml b/paths/generateid.yaml new file mode 100644 index 0000000..9aaeef2 --- /dev/null +++ b/paths/generateid.yaml @@ -0,0 +1,32 @@ + get: + responses: + "200": + description: The generation job has finished (done or failed). + content: + application/json: + schema: + $ref: "../schemas/responses/generationresponse.yaml#/GenerationResponse" + "202": + description: The generation job is still processing. + content: + application/json: + schema: + $ref: "../schemas/responses/generationresponse.yaml#/GenerationResponse" + description: | + Get the status of an on-demand asset generation job created with the + generate endpoint. Jobs are owner-scoped. + + **Base URL:** https://api.shotstack.io/edit/{version} + summary: Get Generation Status + operationId: getGenerate + security: + - DeveloperKey: [] + tags: + - Edit + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The generation job id returned by the generate endpoint. diff --git a/schemas/audioasset.yaml b/schemas/audioasset.yaml index 1a3a879..178ce26 100644 --- a/schemas/audioasset.yaml +++ b/schemas/audioasset.yaml @@ -1,15 +1,19 @@ AudioAsset: description: | The AudioAsset adds audio to a Clip. The audio can be sourced from a URL - (`src`) or generated from a text prompt (`prompt`). Exactly one of `src` or - `prompt` must be provided. + (`src`), generated from a text prompt (`prompt`), or both. At least one of + `src` or `prompt` must be provided. - **Source URL:** set `src` to a publicly accessible audio URL (e.g. mp3). - - **Generated speech:** set `prompt` to the spoken text and `voice` to a voice - identifier (text-to-speech). Optionally set `language`/`newscaster`. - - **Generated music or SFX:** set `prompt` describing the sound; omit `voice`. - - Use `model` to choose the generator. The generated `src` is filled in - automatically. + - **Generated speech:** set `prompt` to the spoken text and choose a + text-to-speech `model`; set the voice via `options`. + - **Generated music or SFX:** set `prompt` describing the sound and choose + a music generation `model`. + - **Both:** `src` acts as a preview placeholder while `prompt` drives + generation — the audio is regenerated from the prompt at render time. + Unchanged prompts and options resolve from the generation cache. + - Use `model` to choose the generator and `options` to configure it. The + generated `src` is filled in automatically. type: object properties: type: @@ -20,65 +24,40 @@ src: description: >- The audio source URL. The URL must be publicly accessible or include - credentials. Provide either `src` or `prompt`, not both. + credentials. When `prompt` is also set, `src` serves as a preview + placeholder and the audio is regenerated from the prompt at render time. type: string minLength: 1 pattern: '\S' example: https://s3-ap-northeast-1.amazonaws.com/my-bucket/sound.mp3 prompt: description: >- - A text prompt. When `voice` is set, the prompt is the spoken text - (text-to-speech). Without `voice`, the prompt describes generated music - or sound effects. The generated `src` is filled in automatically. + A text prompt. For text-to-speech models the prompt is the spoken + text; for music models it describes the sound to generate. The + generated `src` is filled in automatically; an existing `src` is + treated as a preview placeholder and replaced. type: string maxLength: 4000 example: Welcome to today's broadcast. - voice: - description: >- - Voice identifier for text-to-speech generation (e.g. `Matthew`, `Joanna`). - Only meaningful when `prompt` is set. - type: string - example: Matthew - language: - description: >- - Optional BCP-47 language code (e.g. `en-US`) for text-to-speech. - Only meaningful when `prompt` and `voice` are set. - type: string - example: en-US - newscaster: - description: >- - Set to `true` to use the voice's newscaster mode when supported. - Only meaningful when `prompt` and `voice` are set. - type: boolean - default: false model: description: >- The generation model to use when `prompt` is set (e.g. `polly-neural`, - `fal/elevenlabs-tts`, `fal/minimax-speech`, `fal/elevenlabs-music`). - Defaults to the platform's preferred generator if omitted. + `elevenlabs-tts`, `elevenlabs-music`). Defaults to + `elevenlabs-tts` (with a default voice) if omitted. Each model's available options are defined + by the model registry. type: string example: polly-neural - musicLengthMs: + options: description: >- - Target duration in milliseconds for generated music (3,000–600,000). - Only meaningful when `prompt` is set and the model is a music generator - (e.g. `fal/elevenlabs-music`). - type: number - minimum: 3000 - maximum: 600000 - example: 30000 - forceInstrumental: - description: >- - Set to `true` to force instrumental music (no vocals). Only meaningful - for music generation models. - type: boolean - default: false - compositionPlan: - description: >- - An optional structured composition plan for music generation. Only - meaningful for music generation models (e.g. `fal/elevenlabs-music`). - type: string - maxLength: 4000 + Model-specific generation settings. Valid keys and values depend on + the chosen `model` and are defined by the model registry. Omitted + options use the model's defaults. Unknown or invalid options are + rejected. + type: object + additionalProperties: true + example: + voice: Matthew + language: en-US trim: description: >- The start trim point of the audio clip, in seconds (defaults to 0). Audio diff --git a/schemas/imageasset.yaml b/schemas/imageasset.yaml index a26018c..5ed108a 100644 --- a/schemas/imageasset.yaml +++ b/schemas/imageasset.yaml @@ -1,12 +1,16 @@ ImageAsset: description: | The ImageAsset adds an image to a Clip. The image can be sourced from a URL - (`src`) or generated from a text prompt (`prompt`). Exactly one of `src` or - `prompt` must be provided. + (`src`), generated from a text prompt (`prompt`), or both. At least one of + `src` or `prompt` must be provided. - **Source URL:** set `src` to the publicly accessible URL of a jpg or png file. - - **Generated:** set `prompt` to describe the image; the engine generates it - using the provider chosen by `model` and fills `src` in automatically. + - **Generated:** set `prompt` to describe the image. Choose a generator with + `model` and configure it with model-specific `options`; the engine fills + `src` in automatically. + - **Both:** `src` acts as a preview placeholder while `prompt` drives + generation — the image is regenerated from the prompt at render time. + Unchanged prompts and options resolve from the generation cache. type: object properties: type: @@ -17,50 +21,39 @@ src: description: >- The image source URL. The URL must be publicly accessible or include - credentials. Provide either `src` or `prompt`, not both. + credentials. When `prompt` is also set, `src` serves as a preview + placeholder and the image is regenerated from the prompt at render time. type: string minLength: 1 pattern: '\S' example: https://s3-ap-northeast-1.amazonaws.com/my-bucket/image.jpg prompt: description: >- - A text prompt to generate the image from. When set without `src`, the - engine generates an image and fills `src` automatically. Use `model` to - choose the generator. + A text prompt to generate the image from. The engine generates an image + at render time and fills `src` automatically; an existing `src` is + treated as a preview placeholder and replaced. Use `model` to choose + the generator and `options` to configure it. type: string maxLength: 4000 example: A serene landscape with a crystal-clear mountain lake at sunrise. model: description: >- The generation model to use when `prompt` is set (e.g. `flux-schnell`, - `fal/flux-schnell`, `fal/nano-banana-2`). Defaults to the platform's - preferred generator if omitted. + `nano-banana-2`). Defaults to `nano-banana-2` if omitted. Each + model's available options are defined by the model registry. type: string example: flux-schnell - resolution: + options: description: >- - Output resolution tier for supported image generation models. - `1K` (default), `2K`, or `4K`. Only meaningful when `prompt` is set. - enum: - - '1K' - - '2K' - - '4K' - type: string - example: 1K - aspectRatio: - description: >- - Aspect ratio for the generated image. Only meaningful when `prompt` - is set and the model supports it. - enum: - - '1:1' - - '16:9' - - '9:16' - - '4:3' - - '3:4' - - '21:9' - - '9:21' - type: string - example: '1:1' + Model-specific generation settings. Valid keys and values depend on + the chosen `model` and are defined by the model registry. Omitted + options use the model's defaults. Unknown or invalid options are + rejected. + type: object + additionalProperties: true + example: + resolution: 1K + aspectRatio: '16:9' crop: $ref: "./crop.yaml#/Crop" additionalProperties: false diff --git a/schemas/responses/generationresponse.yaml b/schemas/responses/generationresponse.yaml new file mode 100644 index 0000000..05837bd --- /dev/null +++ b/schemas/responses/generationresponse.yaml @@ -0,0 +1,36 @@ + GenerationResponse: + description: >- + The status of an on-demand asset generation job. Completed jobs include + the public URL of the generated asset. + type: object + properties: + id: + description: >- + The generation job id. Deterministic for a given owner and asset + payload (or idempotency key), so identical requests return the same + job and cached result. + type: string + example: 8a1f2c3d-4e5b-5a6c-9d7e-1f2a3b4c5d6e + status: + description: The status of the generation job. + type: string + enum: + - queued + - processing + - done + - failed + example: done + url: + description: >- + The public URL of the generated asset. Present only when `status` + is `done`. + type: string + example: https://shotstack-api-v1-output.s3-ap-southeast-2.amazonaws.com/owner/8a1f2c3d.png + error: + description: >- + A human readable error message. Present only when `status` is + `failed`. + type: string + required: + - id + - status diff --git a/schemas/videoasset.yaml b/schemas/videoasset.yaml index 72df2e5..1bb5505 100644 --- a/schemas/videoasset.yaml +++ b/schemas/videoasset.yaml @@ -1,14 +1,17 @@ VideoAsset: description: | The VideoAsset adds a video to a Clip. The video can be sourced from a URL - (`src`) or generated from a text prompt (`prompt`), optionally from a - starting image (`inputSrc`). Exactly one of `src` or `prompt` must be provided. + (`src`), generated from a text prompt (`prompt`), or both. At least one of + `src` or `prompt` must be provided. - **Source URL:** set `src` to the URL of an mp4 (or compatible) video file. - - **Generated:** set `prompt` to describe the motion. Optionally set `inputSrc` - to a starting image URL (image-to-video). Use `model` to choose the generator - (e.g. `luma-ray-3`, `runpod-itv-mini`). The generated `src` is filled in + - **Generated:** set `prompt` to describe the motion. Choose a generator + with `model` and configure it with model-specific `options` (including a + starting image for image-to-video). The generated `src` is filled in automatically. + - **Both:** `src` acts as a preview placeholder while `prompt` drives + generation — the video is regenerated from the prompt at render time. + Unchanged prompts and options resolve from the generation cache. type: object properties: type: @@ -19,87 +22,42 @@ src: description: >- The video source URL. The URL must be publicly accessible or include - credentials. Provide either `src` or `prompt`, not both. + credentials. When `prompt` is also set, `src` serves as a preview + placeholder and the video is regenerated from the prompt at render time. type: string minLength: 1 pattern: '\S' example: https://s3-ap-northeast-1.amazonaws.com/my-bucket/video.mp4 prompt: description: >- - A text prompt to generate the video from. When set without `src`, the - engine generates a video and fills `src` automatically. Optionally pair - with `inputSrc` for image-to-video. Use `model` to choose the generator. + A text prompt to generate the video from. The engine generates a video + at render time and fills `src` automatically; an existing `src` is + treated as a preview placeholder and replaced. Use `model` to choose + the generator and `options` to configure it (including a starting + image for image-to-video). type: string maxLength: 4000 example: Slowly zoom out and orbit left around the object. - inputSrc: - description: >- - Input image URL for image-to-video generation. The image is used as the - starting frame; `prompt` describes the motion. Has no effect unless - `prompt` is set. - type: string - minLength: 1 - example: https://s3-ap-northeast-1.amazonaws.com/my-bucket/input-image.jpg model: description: >- - The generation model to use when `prompt` is set (e.g. `luma-ray-3`, - `runpod-itv-mini`, `fal/seedance-2.0`). Defaults to the platform's - preferred generator if omitted. + The generation model to use when `prompt` is set (e.g. + `shotstack-itv-mini`, `ray-flash-2`, `seedance-2.0`). Defaults to + `seedance-2.0` if omitted. Each model's available options are + defined by the model registry. type: string - example: luma-ray-3 - resolution: + example: seedance-2.0 + options: description: >- - Output resolution for video generation. Only meaningful when `prompt` - is set and the model supports it (e.g. `fal/seedance-2.0`). - enum: - - '480p' - - '720p' - - '1080p' - type: string - example: '720p' - duration: - description: >- - Target video duration in seconds for generation models that accept - it. `"auto"` lets the model decide. Only meaningful when `prompt` - is set. - enum: - - 'auto' - - '4' - - '5' - - '6' - - '7' - - '8' - - '9' - - '10' - - '11' - - '12' - - '13' - - '14' - - '15' - type: string - default: auto - example: '5' - aspectRatio: - description: >- - Aspect ratio for the generated video. Only meaningful when `prompt` - is set and the model supports it. - enum: - - '1:1' - - '16:9' - - '9:16' - - '4:3' - - '3:4' - - '21:9' - - '9:21' - type: string - example: '16:9' - generateAudio: - description: >- - Set to `true` to request audio generation alongside the video. Only - meaningful for video generation models that support it (e.g. - `fal/seedance-2.0`). - type: boolean - default: false + Model-specific generation settings. Valid keys and values depend on + the chosen `model` and are defined by the model registry. Omitted + options use the model's defaults. Unknown or invalid options are + rejected. + type: object + additionalProperties: true + example: + resolution: 720p + duration: '8' + generateAudio: true transcode: type: boolean description: >- diff --git a/tests/smoke.cjs b/tests/smoke.cjs index ce5c651..118a02c 100644 --- a/tests/smoke.cjs +++ b/tests/smoke.cjs @@ -198,27 +198,27 @@ async function run() { assert.strictEqual(result.model, "flux-schnell"); }); - check("Parse videoAsset with prompt + inputSrc", () => { + check("Parse videoAsset with prompt + options.inputSrc", () => { const result = zodCjs.videoAssetSchema.parse({ type: "video", prompt: "Slowly zoom out and orbit left around the object", - inputSrc: "https://example.com/input-image.jpg", + options: { inputSrc: "https://example.com/input-image.jpg" }, }); assert.strictEqual(result.prompt, "Slowly zoom out and orbit left around the object"); - assert.strictEqual(result.inputSrc, "https://example.com/input-image.jpg"); + assert.strictEqual(result.options.inputSrc, "https://example.com/input-image.jpg"); }); - check("Parse videoAsset with prompt + inputSrc + model", () => { + check("Parse videoAsset with prompt + model + options", () => { const result = zodCjs.videoAssetSchema.parse({ type: "video", prompt: "Camera pans right", - inputSrc: "https://example.com/input.jpg", - model: "luma-ray-3", + model: "seedance-2.0", + options: { inputSrc: "https://example.com/input.jpg" }, }); - assert.strictEqual(result.model, "luma-ray-3"); + assert.strictEqual(result.model, "seedance-2.0"); }); - check("REJECT videoAsset with removed `seed` field (use inputSrc instead)", () => { + check("REJECT videoAsset with removed `seed` field (generation inputs live in options)", () => { assert.throws(() => zodCjs.videoAssetSchema.parse({ type: "video", @@ -228,26 +228,24 @@ async function run() { ); }); - check("Parse audioAsset with prompt + voice", () => { + check("Parse audioAsset with prompt + options.voice", () => { const result = zodCjs.audioAssetSchema.parse({ type: "audio", prompt: "This is a text-to-speech example", - voice: "Matthew", + options: { voice: "Matthew" }, }); assert.strictEqual(result.prompt, "This is a text-to-speech example"); - assert.strictEqual(result.voice, "Matthew"); + assert.strictEqual(result.options.voice, "Matthew"); }); - check("Parse audioAsset with prompt + voice + language + newscaster", () => { + check("Parse audioAsset with speech options (voice, language, newscaster)", () => { const result = zodCjs.audioAssetSchema.parse({ type: "audio", prompt: "Breaking news from around the world", - voice: "Matthew", - language: "en-US", - newscaster: true, + options: { voice: "Matthew", language: "en-US", newscaster: true }, }); - assert.strictEqual(result.language, "en-US"); - assert.strictEqual(result.newscaster, true); + assert.strictEqual(result.options.language, "en-US"); + assert.strictEqual(result.options.newscaster, true); }); check("Parse audioAsset with prompt only (music/SFX generator)", () => { @@ -289,14 +287,15 @@ async function run() { console.log("\n--- Unified asset: src-or-prompt rule (ADR 0001) ---\n"); - // At-least-one: src OR prompt required on image/video/audio. Both allowed. - // Neither → rejected. inputSrc/voice are modifiers and never satisfy the rule. + // At-least-one: src OR prompt required on image/video/audio. Both allowed: + // src is a preview placeholder, prompt regenerates at render. + // Neither → rejected. The options object never satisfies the rule. check("image with src only is valid", () => { zodCjs.imageAssetSchema.parse({ type: "image", src: "https://example.com/a.jpg" }); }); - check("image with both src and prompt is valid (src wins downstream)", () => { + check("image with both src and prompt is valid (src previews; prompt regenerates at render)", () => { zodCjs.imageAssetSchema.parse({ type: "image", src: "https://example.com/a.jpg", @@ -304,6 +303,40 @@ async function run() { }); }); + check("image accepts model-scoped options object", () => { + zodCjs.imageAssetSchema.parse({ + type: "image", + prompt: "a serene lake", + model: "flux-schnell", + options: { resolution: "2K", aspectRatio: "16:9" }, + }); + }); + + check("video accepts model-scoped options object", () => { + zodCjs.videoAssetSchema.parse({ + type: "video", + prompt: "slow orbit", + options: { duration: "8", generateAudio: true }, + }); + }); + + check("REJECT removed flat generation field on video", () => { + assert.throws(() => zodCjs.videoAssetSchema.parse({ + type: "video", + prompt: "slow orbit", + resolution: "720p", + })); + }); + + check("audio accepts speech options", () => { + zodCjs.audioAssetSchema.parse({ + type: "audio", + prompt: "Welcome to the show", + model: "polly-neural", + options: { voice: "Matthew", language: "en-US" }, + }); + }); + check("REJECT image with neither src nor prompt", () => { assert.throws(() => zodCjs.imageAssetSchema.parse({ type: "image" })); }); @@ -316,9 +349,9 @@ async function run() { assert.throws(() => zodCjs.videoAssetSchema.parse({ type: "video" })); }); - check("REJECT video with inputSrc but no src and no prompt (inputSrc is a modifier)", () => { + check("REJECT video with only options and no src/prompt (options never satisfies the rule)", () => { assert.throws(() => - zodCjs.videoAssetSchema.parse({ type: "video", inputSrc: "https://example.com/input.jpg" }) + zodCjs.videoAssetSchema.parse({ type: "video", options: { inputSrc: "https://example.com/input.jpg" } }) ); }); @@ -328,12 +361,12 @@ async function run() { ); }); - check("REJECT audio with voice but no src and no prompt (voice is a modifier)", () => { - assert.throws(() => zodCjs.audioAssetSchema.parse({ type: "audio", voice: "Matthew" })); + check("REJECT audio with only options and no src/prompt (options never satisfies the rule)", () => { + assert.throws(() => zodCjs.audioAssetSchema.parse({ type: "audio", options: { voice: "Matthew" } })); }); - check("audio with prompt + voice is valid (text-to-speech shape)", () => { - zodCjs.audioAssetSchema.parse({ type: "audio", prompt: "hello", voice: "Matthew" }); + check("audio with prompt + options voice is valid (text-to-speech shape)", () => { + zodCjs.audioAssetSchema.parse({ type: "audio", prompt: "hello", options: { voice: "Matthew" } }); }); console.log("\n--- Deprecation markers in bundled spec ---\n");