{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/audivo-dev/json-schema/audivo-dev-transcript-read-schema.json",
  "title": "TranscriptRead",
  "description": "A synchronous JSON transcript read: a cache hit or publisher passthrough (`POST /v1/transcripts`), a direct cached fetch (`GET /v1/episodes/{episode_id}/transcript`), or the re-delivery of a read that was already paid for (`GET /v1/reads/{read_id}`). `credits_charged` is what *this* request charged, which is why it is `0` on the last of the three and the cached-read price for this account on the other two. `format` is always `json` here — a `text|srt|vtt|md` read of the same transcript is delivered as a raw body with `X-Credits-Charged` carrying this envelope's `credits_charged`. See the comment above for why this shape is self-contained rather than composed via `allOf`.",
  "x-generated": "2026-10-30",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/audivo-dev-openapi.yaml#/components/schemas/TranscriptRead",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "format",
    "is_cached",
    "read_id",
    "credits_charged",
    "show_title",
    "episode_title"
  ],
  "properties": {
    "format": {
      "const": "json"
    },
    "is_cached": {
      "type": "boolean"
    },
    "read_id": {
      "$ref": "#/$defs/JobId",
      "description": "The receipt this read was charged under (0.15.0). `GET /v1/reads/{read_id}` delivers the same transcript again, in any format, and charges nothing further."
    },
    "credits_charged": {
      "$ref": "#/$defs/CreditAmount"
    },
    "show_title": {
      "$ref": "#/$defs/JobShowTitle"
    },
    "episode_title": {
      "$ref": "#/$defs/JobEpisodeTitle"
    },
    "transcript": {
      "$ref": "#/$defs/CanonicalTranscript"
    },
    "transcript_url": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048
    },
    "expires_at": {
      "type": "string",
      "format": "date-time"
    }
  },
  "oneOf": [
    {
      "required": [
        "transcript"
      ]
    },
    {
      "required": [
        "transcript_url",
        "expires_at"
      ]
    }
  ],
  "$defs": {
    "CanonicalTranscript": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "episode_id",
        "show_id",
        "language",
        "duration_sec",
        "source",
        "source_revision",
        "model_version",
        "pipeline_version",
        "timing_precision",
        "diarized",
        "warnings",
        "segments",
        "created_at"
      ],
      "properties": {
        "episode_id": {
          "$ref": "#/$defs/EpisodeId"
        },
        "show_id": {
          "$ref": "#/$defs/ShowId"
        },
        "language": {
          "$ref": "#/$defs/Language",
          "description": "Resolved BCP-47 language code."
        },
        "duration_sec": {
          "type": "number",
          "minimum": 0,
          "maximum": 129600
        },
        "source": {
          "$ref": "#/$defs/TranscriptSource"
        },
        "source_revision": {
          "type": "string",
          "minLength": 1,
          "maxLength": 150,
          "description": "e.g. `sha256:...` — the asset revision this transcript was produced from."
        },
        "model_version": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100,
          "description": "Provider-pinned model version, or `publisher` for passthrough."
        },
        "pipeline_version": {
          "type": "string",
          "minLength": 1,
          "maxLength": 60
        },
        "timing_precision": {
          "$ref": "#/$defs/TimingPrecision"
        },
        "diarized": {
          "const": false,
          "description": "Always `false` in the MVP; diarization is post-MVP."
        },
        "warnings": {
          "type": "array",
          "maxItems": 1000,
          "items": {
            "$ref": "#/$defs/TranscriptWarning"
          }
        },
        "segments": {
          "type": "array",
          "maxItems": 200000,
          "items": {
            "$ref": "#/$defs/TranscriptSegment"
          }
        },
        "created_at": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "CreditAmount": {
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991,
      "description": "A non-negative JavaScript-safe integer credit amount. The upper bound matches `Number.MAX_SAFE_INTEGER`, which is enforced by the domain ledger so arithmetic and JSON round-trips cannot silently lose cents worth of credit precision."
    },
    "EpisodeId": {
      "type": "string",
      "pattern": "^ep_[a-z2-7]{16}$",
      "description": "Canonical episode identifier."
    },
    "JobEpisodeTitle": {
      "description": "The episode's title as it was when this job or read was created, or `null` when it was created without one. For an upload it is the title given when the file was announced. Where a feed gave no title, a catalog read (`GET /v1/episodes/{episode_id}/transcript`) answers the identifier the catalog stores in its place.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 300
        },
        {
          "type": "null"
        }
      ]
    },
    "JobId": {
      "type": "string",
      "pattern": "^job_[A-Za-z0-9]{16,32}$",
      "maxLength": 40,
      "description": "Opaque, server-generated job identifier."
    },
    "JobShowTitle": {
      "description": "The show's title as it was when this job or read was created (ADR-0036), or `null` when it was created without one: before 0.14.0, or from a feed that names no show. For an upload it is `Uploads`. Copied once, never updated.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 300
        },
        {
          "type": "null"
        }
      ]
    },
    "Language": {
      "type": "string",
      "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$",
      "maxLength": 35,
      "description": "A BCP-47 language tag (general syntax check, not full IANA subtag validation)."
    },
    "ShowId": {
      "type": "string",
      "pattern": "^sh_[a-z2-7]{16}$",
      "description": "Canonical show identifier."
    },
    "TimingPrecision": {
      "type": "string",
      "enum": [
        "word",
        "segment",
        "none"
      ],
      "description": "How precisely segments/words are timed. ASR output targets `word`. Publisher-sourced transcripts report the precision the source actually provides and never claim word timing they do not have: plain text is `none`, segment-timed sources are `segment`, and only genuine word-timed sources are `word`."
    },
    "TranscriptSegment": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "start",
        "end",
        "speaker",
        "text"
      ],
      "properties": {
        "id": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1000000
        },
        "start": {
          "type": "number",
          "minimum": 0,
          "maximum": 129600,
          "description": "Start time in seconds."
        },
        "end": {
          "type": "number",
          "minimum": 0,
          "maximum": 129600,
          "description": "End time in seconds."
        },
        "speaker": {
          "enum": [
            null
          ],
          "description": "Always `null` in the MVP — diarization is post-MVP. A future diarized-speaker field is a deliberate, reviewed type change, not a silent widening."
        },
        "text": {
          "type": "string",
          "maxLength": 20000
        },
        "words": {
          "type": "array",
          "maxItems": 20000,
          "items": {
            "$ref": "#/$defs/TranscriptWord"
          },
          "description": "Present when `timing_precision` is `word`; absent otherwise."
        }
      }
    },
    "TranscriptSource": {
      "type": "string",
      "enum": [
        "qwen3-asr",
        "whisper-large-v3-turbo",
        "whisper-large-v3",
        "publisher"
      ],
      "description": "The ASR engine that produced this transcript, or `publisher` for passthrough."
    },
    "TranscriptWarning": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "segment",
        "type",
        "detail"
      ],
      "description": "A quality warning from the pipeline's own checks, attached to the segment it concerns. Presented as a heuristic, not an accuracy guarantee; the transcript is delivered and charged as usual. `type` is an open string rather than a closed enum; the documentation's output-formats page lists the values in use.",
      "properties": {
        "segment": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1000000
        },
        "type": {
          "type": "string",
          "minLength": 1,
          "maxLength": 60
        },
        "detail": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500
        }
      }
    },
    "TranscriptWord": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "w",
        "s",
        "e"
      ],
      "properties": {
        "w": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200,
          "description": "The word text."
        },
        "s": {
          "type": "number",
          "minimum": 0,
          "maximum": 129600,
          "description": "Start time in seconds."
        },
        "e": {
          "type": "number",
          "minimum": 0,
          "maximum": 129600,
          "description": "End time in seconds."
        }
      }
    }
  }
}