{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/bisque-cloud/json-schema/bisque-cloud-publish-narrated-request-schema.json",
  "title": "PublishNarratedRequest",
  "description": "`CreatePresentationRequest` plus per-slide narration metadata. The MP3 bytes are NOT in this body: the reply returns one signed `uploadUrls[]` entry per file, you PUT the bytes there, then POST `completeUrl` with the echoed `files`.",
  "x-generated": "2026-11-02",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/_original/bisque-cloud-api-openapi.json#/components/schemas/PublishNarratedRequest",
  "type": "object",
  "required": [
    "indexHtml"
  ],
  "allOf": [
    {
      "$ref": "#/$defs/CreatePresentationRequest"
    }
  ],
  "properties": {
    "audio": {
      "type": "array",
      "description": "One entry per narrated slide you synthesized. Omit a slide to carry its last-published audio forward when its narration text is unchanged; omit the array to carry every slide forward.",
      "items": {
        "type": "object",
        "required": [
          "words",
          "durationMs",
          "size",
          "hash"
        ],
        "properties": {
          "slideKey": {
            "type": "string",
            "description": "The slide's key from the narration plan (the MP3 filename stem). Either this or `slideIndex`."
          },
          "slideIndex": {
            "type": "integer",
            "description": "0-based index of the slide in the HTML."
          },
          "words": {
            "type": "array",
            "description": "Word timings from the synthesizer, in narration order; must match the slide's narration word for word.",
            "items": {
              "type": "object",
              "required": [
                "word",
                "start",
                "end"
              ],
              "properties": {
                "word": {
                  "type": "string"
                },
                "start": {
                  "type": "number",
                  "description": "Seconds."
                },
                "end": {
                  "type": "number",
                  "description": "Seconds."
                }
              }
            }
          },
          "durationMs": {
            "type": "integer",
            "description": "Length of the MP3."
          },
          "size": {
            "type": "integer",
            "description": "Encoded MP3 bytes."
          },
          "hash": {
            "type": "string",
            "description": "sha256 of the MP3 bytes; verified after upload."
          },
          "contentType": {
            "type": "string",
            "const": "audio/mpeg",
            "description": "Optional; anything but MP3 is refused."
          }
        }
      }
    },
    "assets": {
      "type": "array",
      "description": "Page assets (fonts, images) as file descriptors; bytes go through `uploadUrls` like the audio.",
      "items": {
        "type": "object",
        "required": [
          "path",
          "size",
          "hash"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Bundle path under assets/."
          },
          "size": {
            "type": "integer"
          },
          "hash": {
            "type": "string",
            "description": "sha256 of the bytes."
          }
        }
      }
    }
  },
  "$defs": {
    "CreatePresentationRequest": {
      "type": "object",
      "required": [
        "indexHtml"
      ],
      "properties": {
        "indexHtml": {
          "type": "string",
          "description": "The whole `html-presentation/v1` file."
        },
        "title": {
          "type": "string",
          "description": "Overrides the title parsed from the HTML."
        },
        "slug": {
          "type": "string",
          "description": "URL slug under your channel. Reuse it to republish."
        },
        "presentationId": {
          "type": "string",
          "description": "Republish an existing presentation by id instead of slug."
        },
        "handle": {
          "type": "string",
          "description": "Which of your channels to publish on; defaults to your account's."
        },
        "dryRun": {
          "type": "boolean",
          "default": false,
          "description": "Sandbox: run every check a real create runs (format, slug and org conflicts, tier cap, credit preflight) and write nothing. The reply is a 200 with status `validated`."
        },
        "visibility": {
          "type": "string",
          "enum": [
            "public",
            "unlisted",
            "private",
            "org"
          ],
          "default": "unlisted",
          "description": "Who can open the watch link. Unlisted unless you say otherwise; `org` publishes into the bisque.team organization named by `org`."
        },
        "org": {
          "type": "string",
          "description": "bisque.team organization slug; publishes into the company space."
        },
        "contextMd": {
          "type": "string",
          "description": "Agent-facing background shipped with the bundle as `context.md`."
        },
        "designMd": {
          "type": "string",
          "description": "Design notes shipped with the bundle."
        },
        "voiceId": {
          "type": "string",
          "description": "Narration voice tag; defaults to the account's saved voice."
        },
        "madeWith": {
          "type": "array",
          "maxItems": 8,
          "items": {
            "type": "string",
            "maxLength": 80
          },
          "description": "Models credited with making this presentation — e.g. [\"Claude Opus 5\", \"Kokoro af_heart\"]. Free text, shown to viewers in the watch page's ⋯ menu. Omit to keep what a previous publish recorded; send `[]` to clear it."
        },
        "description": {
          "type": "string",
          "maxLength": 5000,
          "description": "Plain-text description shown under the player on the watch page, with web URLs linked — a summary, then the sources. Omit to keep what a previous publish sent; send an empty string to clear it."
        },
        "speechSpeed": {
          "type": "number",
          "description": "Narration speed. 1.0 is normal."
        },
        "voiceStability": {
          "type": "number",
          "description": "How closely the voice keeps to its reference, for voices that take it. Omit for the voice's default."
        },
        "assets": {
          "type": "object",
          "additionalProperties": true,
          "description": "Page assets (fonts, images) keyed by bundle path."
        }
      }
    }
  }
}