openapi: 3.1.0
info:
  title: HieStudio Remote API
  version: 1.0.0
  description: |
    HieStudio Remote API v1 for Text-to-Speech and Speech-to-Text integrations.
    Bearer authentication with a Remote API key (hsk_...) is required on every request.
    TTS/STT payloads follow the same v1 contract as HieStudio Local API. Requests carrying a
    browser Origin header are rejected; this API is intended for native/CLI/backend tools.
servers:
  - url: https://your-hiestudio-domain.example/v1
    description: Replace with your HieStudio domain.
security:
  - bearerAuth: []

paths:
  /health:
    get:
      summary: Check Remote API health
      responses:
        '200':
          description: Remote API is available
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, const: ok }
                  service: { type: string, const: hiestudio-remote-api }
                  apiVersion: { type: string, const: v1 }
        '401': { $ref: '#/components/responses/ApiError' }
        '402': { $ref: '#/components/responses/ApiError' }
  /capabilities:
    get:
      summary: Read current TTS/STT capabilities
      responses:
        '200':
          description: Current entitlements and installed model state
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiVersion: { type: string }
                  authEnabled: { type: boolean }
                  ttsEnabled: { type: boolean }
                  sttEnabled: { type: boolean }
                  models:
                    type: object
                    properties:
                      ttsInstalled: { type: boolean }
                      sttInstalled: { type: boolean }
        '401': { $ref: '#/components/responses/ApiError' }
        '402': { $ref: '#/components/responses/ApiError' }
  /voices:
    get:
      summary: List voices usable by the signed-in account
      parameters:
        - { name: search, in: query, schema: { type: string } }
        - { name: languageCode, in: query, schema: { type: string } }
        - { name: gender, in: query, schema: { type: string } }
        - { name: scope, in: query, schema: { type: string } }
        - { name: sortBy, in: query, schema: { type: string } }
        - { name: sortDirection, in: query, schema: { type: string, enum: [asc, desc] } }
        - { name: page, in: query, schema: { type: integer, minimum: 1 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100 } }
      responses:
        '200':
          description: Voice page
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/Voice' }
                  meta: { type: object, additionalProperties: true }
        '401': { $ref: '#/components/responses/ApiError' }
        '402': { $ref: '#/components/responses/ApiError' }
  /tts/jobs:
    post:
      summary: Queue a Text-to-Speech job
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TtsRequest' }
      responses:
        '200':
          description: Queued job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '401': { $ref: '#/components/responses/ApiError' }
        '402': { $ref: '#/components/responses/ApiError' }
        '409': { $ref: '#/components/responses/ApiError' }
        '422': { $ref: '#/components/responses/ApiError' }
        '429': { $ref: '#/components/responses/ApiError' }
  /stt/jobs:
    post:
      summary: Upload media and queue a Speech-to-Text job
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
                language: { type: string, default: auto }
                punctuation: { type: boolean, default: true }
                cropStartSeconds: { type: number, minimum: 0 }
                cropEndSeconds: { type: number, exclusiveMinimum: 0 }
                diarizationEnabled: { type: boolean, default: true }
                speakerCountMode: { type: string, enum: [auto, exact], default: auto }
                speakerCount: { type: integer, minimum: 1, maximum: 32 }
      responses:
        '200':
          description: Queued job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '401': { $ref: '#/components/responses/ApiError' }
        '402': { $ref: '#/components/responses/ApiError' }
        '409': { $ref: '#/components/responses/ApiError' }
        '413': { $ref: '#/components/responses/ApiError' }
        '422': { $ref: '#/components/responses/ApiError' }
        '429': { $ref: '#/components/responses/ApiError' }
  /jobs/{id}:
    get:
      summary: Read Remote API job status/result
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Job state
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '404': { $ref: '#/components/responses/ApiError' }
  /jobs/{id}/cancel:
    post:
      summary: Cancel an active Remote API job
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Cancelled job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '404': { $ref: '#/components/responses/ApiError' }
        '422': { $ref: '#/components/responses/ApiError' }
  /jobs/{id}/output:
    get:
      summary: Download a completed TTS audio output
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Audio output
          content:
            audio/wav: { schema: { type: string, format: binary } }
            audio/mpeg: { schema: { type: string, format: binary } }
            audio/aac: { schema: { type: string, format: binary } }
            audio/flac: { schema: { type: string, format: binary } }
        '404': { $ref: '#/components/responses/ApiError' }
        '409': { $ref: '#/components/responses/ApiError' }
  /jobs/{id}/transcript:
    get:
      summary: Export a completed STT transcript
      parameters:
        - $ref: '#/components/parameters/JobId'
        - name: format
          in: query
          schema:
            type: string
            enum: [txt, srt, vtt, json-simple, json-full]
            default: json-full
      responses:
        '200':
          description: Transcript in the requested format
        '404': { $ref: '#/components/responses/ApiError' }
        '409': { $ref: '#/components/responses/ApiError' }
        '422': { $ref: '#/components/responses/ApiError' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: HieStudio Remote API Key (hsk_...)
  parameters:
    JobId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
  responses:
    ApiError:
      description: API error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
  schemas:
    TtsRequest:
      type: object
      additionalProperties: false
      required: [voiceId, text]
      properties:
        voiceId: { type: string, format: uuid }
        text: { type: string, minLength: 1, maxLength: 100000 }
        language: { type: string, default: auto }
        inputFormat: { type: string, enum: [txt, srt, vtt], default: txt }
        outputFormat: { type: string, enum: [wav, mp3, aac, flac], default: wav }
        sampleRate: { type: integer, enum: [16000, 22050, 24000, 32000, 44100, 48000], default: 24000 }
        bitrateKbps: { type: [integer, 'null'], enum: [64, 96, 128, 192, 256, 320, null], default: 128 }
        audioChannels: { type: string, enum: [mono, stereo], default: mono }
        steps: { type: integer, minimum: 8, maximum: 64, default: 32 }
        guidanceScale: { type: number, minimum: 0.5, maximum: 5, default: 2 }
        speed: { type: number, minimum: 0.5, maximum: 2, default: 1 }
        duration: { type: [number, 'null'], exclusiveMinimum: 0, default: null }
        denoise: { type: boolean, default: true }
        preprocessPrompt: { type: boolean, default: true }
        postprocessOutput: { type: boolean, default: true }
        trimSilence: { type: boolean, default: true }
    Job:
      type: object
      required: [id, name, kind, status, progress, createdAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        kind: { type: string, enum: [tts, stt] }
        status: { type: string, enum: [queued, loading_model, processing, succeeded, failed, cancelled, interrupted] }
        progress: { type: number, minimum: 0, maximum: 100 }
        phase: { type: [string, 'null'] }
        backend: { type: [string, 'null'] }
        result: { type: [object, 'null'], additionalProperties: true }
        error: { type: [string, 'null'] }
        createdAt: { type: integer, format: int64 }
        startedAt: { type: [integer, 'null'], format: int64 }
        finishedAt: { type: [integer, 'null'], format: int64 }
    Voice:
      type: object
      required: [id, name, languageCode, availability, access]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        languageCode: { type: string }
        gender: { type: string }
        ageGroup: { type: string }
        creationMode: { type: string }
        availability: { type: string }
        access: { type: object, additionalProperties: true }
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            feature: { type: string }
