Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/scripts/published-drift-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment thread
yakimoto marked this conversation as resolved.
}
},
{
"path": "/videos/{videoId}/chapters/{chapterId}",
"method": "DELETE",
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions contract-drift.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -26,7 +26,7 @@
"liveProbed": 0,
"sharedDrift": 0,
"draftNotYetPublished": 0,
"allowlisted": 9,
"allowlisted": 10,
"lapsedAllowlistEntries": 0,
"unmatchedAllowlistEntries": 0
},
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 7 additions & 3 deletions generated/api-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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"];
Expand Down
30 changes: 20 additions & 10 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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
Comment thread
yakimoto marked this conversation as resolved.
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.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
stability:
type: number
minimum: 0
Expand Down
Loading