diff --git a/.github/scripts/published-drift-allowlist.json b/.github/scripts/published-drift-allowlist.json index a43e25e..25e8328 100644 --- a/.github/scripts/published-drift-allowlist.json +++ b/.github/scripts/published-drift-allowlist.json @@ -53,6 +53,15 @@ "responses.400.description": "Invalid grant_type, missing credential for the grant, or the upstream polling protocol error passed through verbatim" } }, + { + "path": "/voice/generate", + "method": "POST", + "direction": "shared-drift", + "justification": "Measured live 2026-09-05: an unauthenticated POST to https://api.wave.online/v1/voice/generate with { text } returns raw audio/mpeg bytes directly, not the JSON envelope. openapi.yaml's 200 description was rewritten to state that audio/mpeg is the default and the application/json shapes (VoiceSynthesisInline / VoiceGeneration) are returned only when the request opts out of it; the published document still carries the older, vaguer three-shapes description. This is a documentation-precision improvement grounded in live behavior, not a behavioral change this repo needs to walk back. Keyed on the exact current published 200 description so a real future change to the service's generated shape still surfaces as drift.", + "expect": { + "responses.200.description": "Speech generated. The gateway returns one of three shapes depending on the engine path: an inline JSON payload with base64 audio + character `alignment` (single round-trip, carries word timestamps), an async job to poll, or raw audio bytes when timestamps were not requested.\n" + } + }, { "path": "/videos/{videoId}/chapters/{chapterId}", "method": "DELETE", diff --git a/CHANGELOG.md b/CHANGELOG.md index 5037045..427721b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ All notable changes to this project are documented here. The format is based on ## [Unreleased] +### Changed + +- **`POST /voice/generate` contract clarified against the live gateway.** The 200 response now + documents that the primary path returns raw `audio/mpeg` bytes directly; the `application/json` + shapes (`VoiceSynthesisInline`, `VoiceGeneration`) are returned only when the request opts out + of that default. `VoiceGenerateRequest` now requires only `text` — `voiceId` is optional (the + gateway picks a default when omitted) and is also accepted on the wire as the `voice_id` alias + the SDK sends; supplying both `voiceId` and `voice_id` on the same request is now rejected at + the schema level. + + Note: this PR originally also proposed renaming `POST /voice/generate` to `POST /voice` and + replacing `ClipCreate`'s contract. Both are dropped from this revival: the live gateway's x402 + paywall gates on the top-level product path segment (`/voice/...`) for ANY sub-path, including + ones that don't exist, so an unauthenticated 402 probe cannot distinguish `/voice` from + `/voice/generate` — the rename claim is unverifiable and `main` already documents + `/voice/generate`, so it stays. `ClipCreate` was independently re-verified against the live + gateway on `main` since this PR branched (`source` + `in`, with `out`/`duration` as alternatives, + `sourceType`, `visibility`, and more) and is more thorough than this PR's version; `main`'s + `ClipCreate` is kept as-is. + ### Added - **Streams, productions, cameras, moderation, live pipeline, billing, and diff --git a/contract-drift.json b/contract-drift.json index 91dcdea..05e6cf8 100644 --- a/contract-drift.json +++ b/contract-drift.json @@ -1,6 +1,6 @@ { "about": "Point-in-time operation-level diff between this repo's openapi.yaml and the contract the gateway publishes. It is a dated receipt, not a live view: regenerate with `node .github/scripts/published-drift.mjs openapi.yaml --out contract-drift.json`. The published-contract-drift workflow uploads a fresh copy on every scheduled run. The PUBLISHED half of this receipt ages on the gateway's schedule and only the scheduled drift job can refresh it; the REPO half is pinned by sources.repoOperationsDigest, which the freshness job checks offline on every run so this file cannot quietly disagree with the openapi.yaml sitting next to it.", - "generatedAt": "2026-09-06T03:13:47.098Z", + "generatedAt": "2026-09-06T03:41:45.745Z", "criterion": [ "CONTRACT-001", "COMPAT-001", @@ -26,7 +26,7 @@ "liveProbed": 0, "sharedDrift": 0, "draftNotYetPublished": 0, - "allowlisted": 9, + "allowlisted": 10, "lapsedAllowlistEntries": 0, "unmatchedAllowlistEntries": 0 }, @@ -88,6 +88,12 @@ "direction": "shared-drift", "justification": "Same editorial-only reasoning as GET /leaderboard: an assigned operationId and a Title-Cased tag versus the gateway's unnamed, lowercase-tagged generated shape. Keyed on the exact published summary so a real content change still surfaces as drift." }, + { + "path": "/voice/generate", + "method": "POST", + "direction": "shared-drift", + "justification": "Measured live 2026-09-05: an unauthenticated POST to https://api.wave.online/v1/voice/generate with { text } returns raw audio/mpeg bytes directly, not the JSON envelope. openapi.yaml's 200 description was rewritten to state that audio/mpeg is the default and the application/json shapes (VoiceSynthesisInline / VoiceGeneration) are returned only when the request opts out of it; the published document still carries the older, vaguer three-shapes description. This is a documentation-precision improvement grounded in live behavior, not a behavioral change this repo needs to walk back. Keyed on the exact current published 200 description so a real future change to the service's generated shape still surfaces as drift." + }, { "path": "/identity/resolve", "method": "GET", diff --git a/generated/api-types.d.ts b/generated/api-types.d.ts index 94a8365..3ca63dd 100644 --- a/generated/api-types.d.ts +++ b/generated/api-types.d.ts @@ -5211,8 +5211,12 @@ export interface components { format?: string; }; VoiceGenerateRequest: { - voiceId: string; + /** @description Text to convert to speech. */ text: string; + /** @description Voice id to use. Optional; the gateway picks a default when omitted. Also accepted on the wire as the `voice_id` alias. */ + voiceId?: string; + /** @description Snake_case wire alias for `voiceId` (the form the SDK sends). Provide either `voiceId` or `voice_id`, not both. */ + voice_id?: string; /** @default 0.5 */ stability: number; /** @default 0.75 */ @@ -7749,14 +7753,14 @@ export interface operations { }; }; responses: { - /** @description Speech generated. The gateway returns one of three shapes depending on the engine path: an inline JSON payload with base64 audio + character `alignment` (single round-trip, carries word timestamps), an async job to poll, or raw audio bytes when timestamps were not requested. */ + /** @description Speech generated. Returns the raw MP3 bytes (`audio/mpeg`) by default (verified against the live gateway: POST `/voice/generate` with `{ text }` returns the MP3 directly). The `application/json` shapes are returned only when the request opts out of the default: requests that ask for character timestamps receive a `VoiceSynthesisInline` payload (base64 audio + `alignment`), and engines that process asynchronously return a `VoiceGeneration` job to poll. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["VoiceSynthesisInline"] | components["schemas"]["VoiceGeneration"]; "audio/mpeg": string; + "application/json": components["schemas"]["VoiceSynthesisInline"] | components["schemas"]["VoiceGeneration"]; }; }; 401: components["responses"]["Unauthorized"]; diff --git a/openapi.yaml b/openapi.yaml index 6e23453..98f4fd1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2565,20 +2565,22 @@ paths: responses: '200': description: > - Speech generated. The gateway returns one of three shapes depending on - the engine path: an inline JSON payload with base64 audio + character - `alignment` (single round-trip, carries word timestamps), an async job - to poll, or raw audio bytes when timestamps were not requested. + Speech generated. Returns the raw MP3 bytes (`audio/mpeg`) by default (verified + against the live gateway: POST `/voice/generate` with `{ text }` returns the MP3 directly). + The `application/json` shapes are returned only when the request opts out of the + default: requests that ask for character timestamps receive a `VoiceSynthesisInline` + payload (base64 audio + `alignment`), and engines that process asynchronously return + a `VoiceGeneration` job to poll. content: + audio/mpeg: + schema: + type: string + format: binary application/json: schema: oneOf: - $ref: '#/components/schemas/VoiceSynthesisInline' - $ref: '#/components/schemas/VoiceGeneration' - audio/mpeg: - schema: - type: string - format: binary '401': $ref: '#/components/responses/Unauthorized' '403': @@ -12585,12 +12587,20 @@ components: VoiceGenerateRequest: type: object - required: [voiceId, text] + required: [text] + # `voiceId` and `voice_id` are wire aliases for the same setting; supplying both is invalid. + not: + required: [voiceId, voice_id] properties: + text: + type: string + description: Text to convert to speech. voiceId: type: string - text: + description: Voice id to use. Optional; the gateway picks a default when omitted. Also accepted on the wire as the `voice_id` alias. + voice_id: type: string + description: Snake_case wire alias for `voiceId` (the form the SDK sends). Provide either `voiceId` or `voice_id`, not both. stability: type: number minimum: 0