# Derived from /openapi.yaml.
# Locale-specific API reference source for en.
# Only x-docs-locale and x-derived-from are injected; endpoint definitions remain canonical.
openapi: 3.0.3
x-docs-locale: en
x-derived-from: /openapi.yaml
info:
  title: Nagovori API
  version: 1.0.0
  description: |
    REST API for speech-to-text transcription and text-to-speech synthesis.

    ## Authentication

    All `/v1/*` endpoints require authentication via one of:
    - **API Key**: Bearer token with `nag_` prefix (recommended for server-to-server)
    - **JWT**: Bearer token from OIDC authentication flow

    ```
    Authorization: Bearer nag_YOUR_API_KEY
    ```

    ## Base URL

    All API requests should be made to:
    ```
    https://api.nagovori.ru
    ```

    ## Rate Limits

    - 60 requests per minute per user
    - 1 concurrent transcription per user

    ## Error Format

    All errors return JSON with an `error` field:
    ```json
    {"error": "human-readable error message"}
    ```
  contact:
    name: Nagovori Support
    url: https://nagovori.ru
  license:
    name: Proprietary

servers:
  - url: https://api.nagovori.ru
    description: Production

tags:
  - name: Upload
    description: File upload via presigned URLs
  - name: Transcription
    description: Speech-to-text transcription
  - name: TTS
    description: Text-to-speech synthesis

security:
  - BearerAuth: []

paths:
  /v1/uploads/presign:
    post:
      tags: [Upload]
      summary: Get a presigned URL for file upload
      description: |
        Returns a presigned URL for uploading an audio file to object storage.
        Upload the file with a PUT request to the returned URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename]
              properties:
                filename:
                  type: string
                  description: Original filename
                  example: meeting.mp3
                content_type:
                  type: string
                  description: MIME type of the file
                  example: audio/mpeg
                size_bytes:
                  type: integer
                  format: int64
                  description: File size in bytes
                  example: 5242880
                content_sha256:
                  type: string
                  description: Optional SHA-256 hash to deduplicate uploads
      responses:
        '200':
          description: Presigned upload details
          content:
            application/json:
              schema:
                type: object
                properties:
                  bucket:
                    type: string
                  object_key:
                    type: string
                    example: uploads/user123/abc123.mp3
                  upload_url:
                    type: string
                    format: uri
                    description: PUT this URL with the file body
                  expires_at:
                    type: string
                    format: date-time
                  already_uploaded:
                    type: boolean
                    description: True if file was already uploaded (dedup by SHA-256)
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/transcriptions:
    post:
      tags: [Transcription]
      summary: Create a transcription job
      description: |
        Starts a new transcription job for an uploaded audio file.
        The file must have been uploaded via the presign endpoint first.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [object_key]
              properties:
                object_key:
                  type: string
                  description: Object key from the presign response
                filename:
                  type: string
                content_type:
                  type: string
                  example: audio/mpeg
                size_bytes:
                  type: integer
                  format: int64
                duration_seconds:
                  type: integer
                  description: Audio duration in seconds (for billing)
                language:
                  type: string
                  description: Language code or "auto" for detection
                  example: auto
                model:
                  type: string
                  description: Gemma ASR model ID
                  enum: [gemma4-e2b-asr, gemma4-e2b-asr-stream]
                chunk_prompt_preset:
                  type: string
                  description: Chunk-level ASR preset applied to each part before transcription
                  enum: [default, verbatim, remove_fillers, concise]
                  default: default
                pricing_mode:
                  type: string
                  description: Pricing mode (free_test or package)
                source_channel:
                  type: string
                  description: Source channel (web, api, telegram, etc.)
      responses:
        '201':
          description: Transcription job created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateTranscriptionResponse'
        '400':
          description: Bad request (file too long, invalid params)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: No transcription credits remaining
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited (concurrent job or daily limit)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      tags: [Transcription]
      summary: List transcriptions
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of transcriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Transcription'
                  count:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/transcriptions/{id}:
    get:
      tags: [Transcription]
      summary: Get a transcription by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Transcription details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transcription'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Transcription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/transcriptions/{id}/stream:
    get:
      tags: [Transcription]
      summary: Stream transcription progress (SSE)
      description: |
        Server-Sent Events stream for real-time transcription progress.

        Events:
        - `update` — full transcription state snapshot
        - `token` — individual text token (incremental)
        - `done` — transcription completed or failed
        - `error` — error occurred
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: SSE event stream
          content:
            text/event-stream:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/transcriptions/{id}/eta:
    get:
      tags: [Transcription]
      summary: Get estimated time for transcription
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: ETA estimate
          content:
            application/json:
              schema:
                type: object
                properties:
                  queue_position:
                    type: integer
                  estimated_wait_seconds:
                    type: integer
                  average_duration_seconds:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/tts/voices:
    get:
      tags: [TTS]
      summary: List available TTS voices
      responses:
        '200':
          description: Available voices
          content:
            application/json:
              schema:
                type: object
                properties:
                  voices:
                    type: array
                    items:
                      $ref: '#/components/schemas/TTSVoice'
                  default:
                    type: string
                    example: alloy
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/tts:
    post:
      tags: [TTS]
      summary: Create a TTS job
      description: Queues a text-to-speech synthesis job.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text:
                  type: string
                  description: Text to synthesize (max 5000 characters)
                  maxLength: 5000
                voice:
                  type: string
                  description: Voice ID
                  example: alloy
                  enum: [alloy, ash, nova, onyx, echo, shimmer, fable]
      responses:
        '201':
          description: TTS job created
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    $ref: '#/components/schemas/TTSJob'
        '400':
          description: Invalid request (text too long, invalid voice)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/tts/stream:
    post:
      tags: [TTS]
      summary: Stream TTS audio
      description: |
        Streams synthesized audio in real-time as chunked MP3.
        Audio begins playing after the first sentence is processed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text:
                  type: string
                  maxLength: 5000
                voice:
                  type: string
                  enum: [alloy, ash, nova, onyx, echo, shimmer, fable]
      responses:
        '200':
          description: Streaming MP3 audio
          content:
            audio/mpeg:
              schema:
                type: string
                format: binary
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/tts/{id}:
    get:
      tags: [TTS]
      summary: Get a TTS job by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: TTS job details
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    $ref: '#/components/schemas/TTSJob'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/tts/{id}/download:
    get:
      tags: [TTS]
      summary: Get download URL for completed TTS audio
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Presigned download URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Use either an API key (starting with `nag_`) or a JWT token.
        Example: `Authorization: Bearer nag_a1b2c3d4e5f67890abcdef1234567890`

  responses:
    Unauthorized:
      description: Authentication required or invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Human-readable error message
          example: unauthorized

    UserProfile:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
        telegram_id:
          type: integer
          nullable: true
        vk_user_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time

    Usage:
      type: object
      properties:
        free_remaining:
          type: integer
          description: Remaining free seconds
        package_seconds_remaining:
          type: integer
          description: Remaining purchased seconds
        total_seconds_used:
          type: integer
          description: Total seconds ever used
    QueueEstimate:
      type: object
      properties:
        queue_position:
          type: integer
        estimated_wait_seconds:
          type: integer
        estimated_started_at:
          type: string
          format: date-time
        average_duration_seconds:
          type: integer

    PricingBreakdown:
      type: object
      properties:
        mode:
          type: string
        seconds:
          type: integer
        reserved_seconds:
          type: integer
        rate_kopeks_per_minute:
          type: integer
          format: int64
        base_amount_kopeks:
          type: integer
          format: int64
        minimum_charge_kopeks:
          type: integer
          format: int64
        charged_amount_kopeks:
          type: integer
          format: int64
        minimum_charge_applied:
          type: boolean
        package_seconds_used:
          type: integer
        package_balance_id:
          type: string
        free_seconds_used:
          type: integer

    CreateTranscriptionResponse:
      type: object
      properties:
        user:
          $ref: '#/components/schemas/UserProfile'
        transcription:
          $ref: '#/components/schemas/Transcription'
        usage:
          $ref: '#/components/schemas/Usage'
        estimate:
          $ref: '#/components/schemas/QueueEstimate'
        quote:
          $ref: '#/components/schemas/PricingBreakdown'

    Model:
      type: object
      properties:
        id:
          type: string
          example: gemma4-e2b-asr-stream
        label:
          type: string
          example: Default
        is_default:
          type: boolean
        description:
          type: string

    Transcription:
      type: object
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        workflow_id:
          type: string
        workflow_run_id:
          type: string
        status:
          type: string
          enum: [queued, processing, completed, failed]
        source_message_id:
          type: integer
          nullable: true
        source_bucket:
          type: string
        source_key:
          type: string
        source_filename:
          type: string
        source_content_type:
          type: string
        source_size_bytes:
          type: integer
          format: int64
        requested_language:
          type: string
        queue_priority:
          type: integer
        duration_seconds:
          type: integer
        detected_language:
          type: string
        language_state:
          $ref: '#/components/schemas/ASRLanguageState'
        model:
          type: string
        transcript_text:
          type: string
          description: Full transcript (available when completed)
        transcript_segments:
          type: array
          items:
            type: object
            properties:
              start:
                type: number
                format: float
              end:
                type: number
                format: float
              text:
                type: string
        error_message:
          type: string
          nullable: true
        pricing_mode:
          type: string
        source_channel:
          type: string
        pricing_breakdown:
          $ref: '#/components/schemas/PricingBreakdown'
        billing_source:
          type: string
        reserved_amount_cents:
          type: integer
          format: int64
        actual_amount_cents:
          type: integer
          format: int64
        billing_usage:
          type: object
        estimated_wait_seconds:
          type: integer
        refunded:
          type: boolean
        created_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time

    TTSVoice:
      type: object
      properties:
        id:
          type: string
          example: alloy
        label:
          type: string
          example: Alloy
        language:
          type: string
          example: ru
        gender:
          type: string
          enum: [male, female]
        description:
          type: string

    TTSJob:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, running, completed, failed]
        text:
          type: string
        voice:
          type: string
        duration_seconds:
          type: number
          format: float
          nullable: true
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true

    APIKey:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        prefix:
          type: string
          example: nag_a1b2
        scopes:
          type: array
          items:
            type: string
        last_used_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    LinkToken:
      type: object
      properties:
        token:
          type: string
          description: Token to send to the bot via /connect command
        expires_in:
          type: integer
          description: Seconds until the token expires
    ASRLanguageState:
      type: object
      properties:
        detected_language:
          type: string
        locked_language:
          type: string
        parts_processed:
          type: integer
        lock_after_parts:
          type: integer
        locked:
          type: boolean
