asyncapi: '2.6.0'
info:
  title: Plio Live Earnings — Streaming Channels
  version: '1.0.0'
  description: |
    Server-Sent Event channels for live, attributed earnings-call transcripts.

    Each channel is a unidirectional HTTP stream — the client opens a GET
    and the server pushes `event:` / `data:` framed messages until the
    call ends or the connection drops.

    Auth is identical to the REST surface (`Authorization: Bearer ek_*`
    or `?token=ek_*`). See `openapi.yaml` for the REST contract.

  contact:
    name: Plio
    email: inder@plio.live

servers:
  production:
    url: pliioai.com
    protocol: https
    description: Production
    security:
      - bearerAuth: []
      - queryToken: []

defaultContentType: application/json

channels:
  /api/live/{slug}/stream:
    description: |
      Finalized attribution events. The canonical subscriber stream.
      Each envelope carries a monotonic `seq`; clients use it to detect
      gaps after a reconnect (then recover via REST `/events?from=`).

      Includes:
        - `attribution` — speaker named, transcript text included
        - `unknown` — turn we couldn't confidently attribute
        - `phase_transition` — section boundary
        - `ai_insight` — model-surfaced moment anchored to a turn
        - `turn_speaker_amend` — late operator correction (in-place
          rename of an already-published turn; dedicated event type
          as of 2026-05-21)
        - `done` — call ended

      Treat the latest signal per `turnId` as canonical. A
      `turn_speaker_amend` overrides the speaker `name` on the
      original turn; the turn keeps its chronological position.
    parameters:
      slug:
        $ref: '#/components/parameters/Slug'
    subscribe:
      operationId: subscribeStream
      message:
        $ref: '#/components/messages/Envelope'
      bindings:
        sse:
          eventName: envelope

  /api/live/{slug}/partial-stream:
    description: |
      In-flight transcript partials. Each speaker turn surfaces here
      with `transcript` growing word-by-word as the speaker speaks.

      When a partial finalizes, an envelope publishes on `/stream` with
      the same `turnId`. Use that to clear your live cursor.

      Partial-stream messages have a deliberately minimal shape — no
      `seq`, no `publishedAt`, no `phase`. They're transient. Don't
      persist them; use them only as a live read-out of the in-flight
      turn.
    parameters:
      slug:
        $ref: '#/components/parameters/Slug'
    subscribe:
      operationId: subscribePartials
      message:
        $ref: '#/components/messages/Partial'
      bindings:
        sse:
          eventName: partial

  /api/live/{slug}/audio-stream:
    description: |
      Raw call audio as base64-encoded PCM chunks. Subscriber-only.

      Format: 16 kHz, 1 channel, signed 16-bit little-endian PCM,
      base64-encoded. Each event carries one chunk (~40-100 ms).
      Concatenate chunks in arrival order to reconstruct the audio.

      Cycle 2 product surface — contact Plio for access.
    parameters:
      slug:
        $ref: '#/components/parameters/Slug'
    subscribe:
      operationId: subscribeAudio
      message:
        $ref: '#/components/messages/AudioChunk'
      bindings:
        sse:
          eventName: audio

components:
  parameters:
    Slug:
      description: Call slug — e.g., `nvda-q4-fy26`.
      schema:
        type: string
        pattern: '^[a-z0-9-]+$'

  messages:
    Envelope:
      name: envelope
      title: Finalized event envelope
      summary: Wire-format envelope shared with REST `/events`.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/EventEnvelope'
      bindings:
        sse:
          eventName: envelope

    Partial:
      name: partial
      title: In-flight transcript partial
      contentType: application/json
      payload:
        type: object
        required: [turnId, transcript]
        properties:
          turnId:
            type: string
            description: |
              Matches the eventual `attribution` envelope's `turnId`
              on `/stream`. Use this to associate the in-flight partial
              with its final form.
          transcript:
            type: string
            description: Growing word-by-word transcript.
      bindings:
        sse:
          eventName: partial

    AudioChunk:
      name: audio
      title: Base64-encoded PCM chunk
      contentType: application/json
      payload:
        type: object
        required: [pcm, publishedAt]
        properties:
          pcm:
            type: string
            description: |
              Base64-encoded raw PCM. 16 kHz, 1 channel, signed 16-bit
              little-endian, no headers.
          publishedAt:
            type: string
            format: date-time
      bindings:
        sse:
          eventName: audio

  schemas:
    Phase:
      type: string
      enum:
        - unknown
        - operator_opening
        - ir_intro
        - cfo_remarks
        - ceo_remarks
        - qa
        - closing

    EventEnvelope:
      type: object
      required: [tenantId, callSlug, seq, publishedAt, event]
      properties:
        tenantId:
          type: string
          example: plio-live
        callSlug:
          type: string
        seq:
          type: integer
        publishedAt:
          type: string
          format: date-time
        event:
          oneOf:
            - $ref: '#/components/schemas/AttributionEvent'
            - $ref: '#/components/schemas/UnknownEvent'
            - $ref: '#/components/schemas/PhaseTransitionEvent'
            - $ref: '#/components/schemas/AiInsightEvent'
            - $ref: '#/components/schemas/TurnSpeakerAmendEvent'
            - $ref: '#/components/schemas/DoneEvent'

    AttributionEvent:
      type: object
      required: [type, turnId, name, source]
      properties:
        type:
          const: attribution
        turnId:
          type: string
        name:
          type: string
        source:
          const: plio-live
        text:
          type: string
        phase:
          $ref: '#/components/schemas/Phase'

    UnknownEvent:
      type: object
      required: [type, turnId]
      properties:
        type:
          const: unknown
        turnId:
          type: string
        text:
          type: string
        phase:
          $ref: '#/components/schemas/Phase'

    PhaseTransitionEvent:
      type: object
      required: [type, fromPhase, toPhase, triggerTurnId]
      properties:
        type:
          const: phase_transition
        fromPhase:
          $ref: '#/components/schemas/Phase'
        toPhase:
          $ref: '#/components/schemas/Phase'
        triggerTurnId:
          type: string

    AiInsightEvent:
      type: object
      required: [type, turnId, insight, category]
      properties:
        type:
          const: ai_insight
        turnId:
          type: string
          description: |
            Anchors back to the attribution event the insight applies
            to. Match against your already-rendered turn.
        insight:
          type: string
          description: One-paragraph synthesis of why this turn matters.
        category:
          type: string
          description: |
            Coarse bucket — e.g. `guidance`, `surprise`, `pressure`,
            `numbers`. New values may be added without notice; treat
            unknown values as an opaque tag, never strict-validate.
        frame:
          type: string
          description: Optional extra context paragraph.
        watchFor:
          type: string
          description: Optional follow-up signal to listen for.

    TurnSpeakerAmendEvent:
      type: object
      required: [type, turnId, newName]
      description: |
        Post-publish operator correction to a previously-published
        turn's speaker label. Apply in-place: the original turn keeps
        its chronological position; only the displayed speaker name
        changes. Latest amend per `turnId` wins.

        Historical note: calls published before 2026-05-21 may carry
        a second `AttributionEvent` on the same `turnId` instead
        (the legacy amend path). Subscribers should treat either
        pattern as a correction.
      properties:
        type:
          const: turn_speaker_amend
        turnId:
          type: string
        newName:
          type: string
        reason:
          type: string
          description: Optional operator-supplied rationale (capped 200 chars).

    DoneEvent:
      type: object
      required: [type]
      properties:
        type:
          const: done

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
    queryToken:
      type: apiKey
      in: query
      name: token
