{
  "openapi": "3.1.0",
  "info": {
    "title": "Sonilo API",
    "version": "1.0.0",
    "description": "REST API for AI music, video-to-music scoring, sound effects, audio ducking, async tasks, and account usage."
  },
  "servers": [
    {
      "url": "https://api.sonilo.com",
      "description": "Production API"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/text-to-music": {
      "post": {
        "summary": "Generate music from a text prompt",
        "description": "Streams application/x-ndjson by default with title, audio_chunk, complete, and error events. Pass mode=async to return a task_id.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/TextToMusicRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "NDJSON stream with title, audio_chunk, complete, and error events.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string",
                  "description": "One JSON event per line."
                }
              }
            }
          },
          "202": {
            "description": "Async task created when mode=async.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        }
      }
    },
    "/v1/video-to-music": {
      "post": {
        "summary": "Generate music for a video",
        "description": "Accepts exactly one of video or video_url. Streams NDJSON by default or returns a task_id when mode=async.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoToMusicRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "NDJSON stream with generated audio chunks."
          },
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/video-to-video-music": {
      "post": {
        "summary": "Generate music for a video and return the finished video",
        "description": "Accepts exactly one of video or video_url. Returns 202 with a task_id whose result is a video with the generated music muxed in. Poll GET /v1/tasks/{task_id}.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoToVideoMusicRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/text-to-sfx": {
      "post": {
        "summary": "Generate a sound effect from text",
        "description": "Returns 202 with a task_id. Poll GET /v1/tasks/{task_id}.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/TextToSfxRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/video-to-sfx": {
      "post": {
        "summary": "Generate synchronized sound effects for a video",
        "description": "Accepts exactly one of video or video_url. Returns 202 with a task_id. Poll GET /v1/tasks/{task_id}.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoToSfxRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/video-to-video-sfx": {
      "post": {
        "summary": "Generate synchronized sound effects for a video and return the finished video",
        "description": "Accepts exactly one of video or video_url. Returns 202 with a task_id whose result is a video with the generated sound effects muxed in. Poll GET /v1/tasks/{task_id}.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoToVideoSfxRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/video-to-sound": {
      "post": {
        "summary": "Generate synchronized music and sound effects for a video",
        "description": "Accepts exactly one of video or video_url. Returns 202 with a task_id whose result is the combined audio track at output_url. Poll GET /v1/tasks/{task_id}.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoToSoundRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/video-to-video-sound": {
      "post": {
        "summary": "Generate synchronized music and sound effects for a video and return the finished video",
        "description": "Accepts exactly one of video or video_url. Returns 202 with a task_id whose result is a video with the combined music and sound effects muxed in at output_url. Poll GET /v1/tasks/{task_id}.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoToVideoSoundRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/dubbing": {
      "post": {
        "summary": "Dub a video's speech into one or more languages",
        "description": "Accepts exactly one of video or video_url; a video_url must use https. Returns 202 with a task_id whose result is an outputs map of language code to dubbed video URL. Poll GET /v1/tasks/{task_id}.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/DubbingRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/audio-ducking": {
      "post": {
        "summary": "Duck music under speech or voice",
        "description": "Accepts one voice input and one music input. Returns 202 with a task_id.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/AudioDuckingRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async task created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAccepted"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tasks/{task_id}": {
      "get": {
        "summary": "Retrieve async task status",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Task status and result URLs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskStatus"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/account/services": {
      "get": {
        "summary": "List enabled services and limits",
        "responses": {
          "200": {
            "description": "Enabled services, rate limits, concurrency, upload size, and discount factor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountServices"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/usage": {
      "get": {
        "summary": "Get account usage",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage summary and daily breakdown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountUsage"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key"
      }
    },
    "schemas": {
      "TextToMusicRequest": {
        "type": "object",
        "required": ["prompt", "duration"],
        "properties": {
          "prompt": {
            "type": "string",
            "description": "Music prompt."
          },
          "duration": {
            "type": "integer",
            "minimum": 5,
            "maximum": 360
          },
          "mode": {
            "type": "string",
            "enum": ["stream", "async"],
            "default": "stream"
          },
          "output_format": {
            "type": "string",
            "enum": ["m4a", "wav"],
            "default": "m4a",
            "description": "Async mode only."
          },
          "segments": {
            "type": "string",
            "description": "JSON string with segment prompts."
          }
        }
      },
      "VideoToMusicRequest": {
        "type": "object",
        "properties": {
          "video": {
            "type": "string",
            "format": "binary"
          },
          "video_url": {
            "type": "string"
          },
          "prompt": {
            "type": "string"
          },
          "segments": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "enum": ["stream", "async"],
            "default": "stream"
          },
          "preserve_speech": {
            "type": "boolean"
          },
          "ducking": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "VideoToVideoMusicRequest": {
        "type": "object",
        "properties": {
          "video": {
            "type": "string",
            "format": "binary"
          },
          "video_url": {
            "type": "string"
          },
          "prompt": {
            "type": "string"
          },
          "preserve_speech": {
            "type": "boolean"
          },
          "isolate_vocals": {
            "type": "boolean",
            "description": "Legacy alias for preserve_speech; the two are ORed together, not separate behaviors."
          }
        }
      },
      "TextToSfxRequest": {
        "type": "object",
        "required": ["prompt", "duration"],
        "properties": {
          "prompt": {
            "type": "string"
          },
          "duration": {
            "type": "integer",
            "minimum": 1,
            "maximum": 180
          },
          "audio_format": {
            "type": "string"
          }
        }
      },
      "VideoToSfxRequest": {
        "type": "object",
        "properties": {
          "video": {
            "type": "string",
            "format": "binary"
          },
          "video_url": {
            "type": "string"
          },
          "prompt": {
            "type": "string"
          },
          "segments": {
            "type": "string"
          },
          "audio_format": {
            "type": "string"
          }
        }
      },
      "VideoToVideoSfxRequest": {
        "type": "object",
        "properties": {
          "video": {
            "type": "string",
            "format": "binary"
          },
          "video_url": {
            "type": "string"
          },
          "prompt": {
            "type": "string"
          },
          "segments": {
            "type": "string"
          }
        }
      },
      "VideoToSoundRequest": {
        "type": "object",
        "properties": {
          "video": {
            "type": "string",
            "format": "binary"
          },
          "video_url": {
            "type": "string"
          },
          "music_prompt": {
            "type": "string"
          },
          "sfx_prompt": {
            "type": "string"
          },
          "segments": {
            "type": "string"
          },
          "preserve_speech": {
            "type": "boolean"
          },
          "isolate_vocals": {
            "type": "boolean",
            "description": "Legacy alias for preserve_speech; the two are ORed together, not separate behaviors."
          },
          "ducking": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "VideoToVideoSoundRequest": {
        "type": "object",
        "properties": {
          "video": {
            "type": "string",
            "format": "binary"
          },
          "video_url": {
            "type": "string"
          },
          "music_prompt": {
            "type": "string"
          },
          "sfx_prompt": {
            "type": "string"
          },
          "segments": {
            "type": "string"
          },
          "preserve_speech": {
            "type": "boolean"
          },
          "isolate_vocals": {
            "type": "boolean",
            "description": "Legacy alias for preserve_speech; the two are ORed together, not separate behaviors."
          },
          "ducking": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "DubbingRequest": {
        "type": "object",
        "properties": {
          "video": {
            "type": "string",
            "format": "binary"
          },
          "video_url": {
            "type": "string",
            "description": "Must use https when supplied instead of video."
          },
          "languages": {
            "type": "string",
            "description": "JSON array of language codes. Defaults to a standard set when omitted."
          }
        }
      },
      "AudioDuckingRequest": {
        "type": "object",
        "properties": {
          "voice_file": {
            "type": "string",
            "format": "binary"
          },
          "voice_url": {
            "type": "string"
          },
          "music_file": {
            "type": "string",
            "format": "binary"
          },
          "music_url": {
            "type": "string"
          }
        }
      },
      "TaskAccepted": {
        "type": "object",
        "required": ["task_id", "status"],
        "properties": {
          "task_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["processing"]
          }
        }
      },
      "TaskStatus": {
        "type": "object",
        "required": ["task_id", "status"],
        "properties": {
          "task_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["processing", "succeeded", "failed"]
          },
          "audio": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MediaResult"
            }
          },
          "video": {
            "$ref": "#/components/schemas/MediaResult"
          },
          "outputs": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Dubbing only: language code to dubbed video URL."
          },
          "output_url": {
            "type": "string",
            "description": "video-to-sound and video-to-video-sound only: URL of the combined output."
          },
          "output_type": {
            "type": "string",
            "enum": ["audio", "video"],
            "description": "video-to-sound and video-to-video-sound only."
          },
          "output_bytes": {
            "type": "integer",
            "description": "video-to-sound and video-to-video-sound only: size in bytes of the file at output_url."
          },
          "music": {
            "$ref": "#/components/schemas/MediaResult"
          },
          "sfx": {
            "$ref": "#/components/schemas/MediaResult"
          },
          "music_processed": {
            "$ref": "#/components/schemas/MediaResult"
          },
          "error": {
            "$ref": "#/components/schemas/Error"
          }
        }
      },
      "MediaResult": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "file_size": {
            "type": "integer"
          },
          "stream_index": {
            "type": "integer"
          }
        }
      },
      "AccountServices": {
        "type": "object",
        "properties": {
          "available_services": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rpm_limit": {
            "type": "integer"
          },
          "concurrency_limit": {
            "type": "integer"
          },
          "discount_factor": {
            "type": "number"
          },
          "max_upload_size_mb": {
            "type": "integer"
          }
        }
      },
      "AccountUsage": {
        "type": "object",
        "properties": {
          "summary": {
            "type": "object"
          },
          "daily": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "payment_required. If message contains Insufficient balance, classify as insufficient_balance and do not retry until balance is added.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "code": "payment_required",
              "message": "Insufficient balance: balance=0.0000 < needed=0.0675."
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "Valid JSON or form data that cannot be processed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "rate_limit_exceeded. Respect Retry-After when present.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UpstreamError": {
        "description": "Transient upstream generation failure.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Task or resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
