openapi: 3.1.0
info:
  title: AiuniVid Open API
  version: 1.0.0-draft
  description: |
    AiuniVid 视频生成开放 API v1（契约草稿，与 `API_CONTRACT_V1.md` 一致）。

    - Base path: `/api/open`
    - 主创建入口: `POST /v1/videos/generations`
    - 认证: `Authorization: Bearer av_live_...`
    - 创建任务必须带 `Idempotency-Key`
    - Scope: `video:create`（创建/报价）、`video:read`（查询任务/模型/余额）、`files:write`（上传预签名）；每个操作的 `x-required-scope` 字段标注所需 scope
    - 限流: 创建类 10/min/key，读取类 60/min/key（见响应头 `X-RateLimit-*`）
  contact:
    name: AiuniVid API
    url: https://aiunivid.com

servers:
  - url: https://aiunivid.com/api/open
    description: Production
  - url: http://localhost:3003/api/open
    description: Local development

tags:
  - name: Models
  - name: Credits
  - name: Generations
  - name: Uploads

security:
  - bearerAuth: []

paths:
  /v1/models:
    get:
      tags: [Models]
      operationId: listModels
      summary: 列出可用模型与能力
      x-required-scope: video:read
      responses:
        "200":
          description: OK
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/credits/balance:
    get:
      tags: [Credits]
      operationId: getBalance
      summary: 查询积分余额
      x-required-scope: video:read
      responses:
        "200":
          description: OK
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BalanceResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/videos/generations/quote:
    post:
      tags: [Generations]
      operationId: quoteGeneration
      summary: 报价（不创建任务、不扣费）
      x-required-scope: video:create
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateGenerationRequest"
      responses:
        "200":
          description: Quote
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QuoteResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/Unprocessable"

  /v1/videos/generations:
    post:
      tags: [Generations]
      operationId: createGeneration
      summary: 创建视频生成任务（主入口）
      x-required-scope: video:create
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateGenerationRequest"
            examples:
              textToVideo:
                summary: 文生视频
                value:
                  model: seedance-2.5-text-to-video
                  prompt: "雨夜旧书店门口，电影感中景跟拍，暖黄台灯与冷霓虹对比"
                  duration: 8
                  quality: "720p"
                  aspect_ratio: "16:9"
                  generate_audio: true
                  callback_url: "https://customer.example.com/hooks/video"
              imageToVideo:
                summary: 图生视频（首尾帧）
                value:
                  model: seedance-2.0-image-to-video
                  prompt: "镜头从首帧平滑过渡到尾帧，保持人物服装一致"
                  image_urls:
                    - "https://cdn.example.com/first.jpg"
                    - "https://cdn.example.com/last.jpg"
                  duration: 5
                  quality: "720p"
                  aspect_ratio: adaptive
              referenceToVideo:
                summary: 多模态参考
                value:
                  model: seedance-2.0-reference-to-video
                  prompt: "@image1 的人物外观保持一致，运镜参考 @video1"
                  image_urls:
                    - "https://cdn.example.com/role.jpg"
                  video_urls:
                    - "https://cdn.example.com/motion.mp4"
                  audio_urls:
                    - "https://cdn.example.com/bgm.mp3"
                  duration: 8
                  quality: "720p"
      responses:
        "202":
          description: 任务已接受
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/Unprocessable"
        "429":
          $ref: "#/components/responses/RateLimited"
    get:
      tags: [Generations]
      operationId: listGenerations
      summary: 任务列表
      x-required-scope: video:read
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/TaskStatus"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/videos/generations/{id}:
    get:
      tags: [Generations]
      operationId: getGeneration
      summary: 查询任务详情
      x-required-scope: video:read
      parameters:
        - $ref: "#/components/parameters/TaskId"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/videos/generations/{id}/cancel:
    post:
      tags: [Generations]
      operationId: cancelGeneration
      summary: 取消任务（已关闭）
      description: 不支持取消。提交后的任务会跑到成功或失败；调用返回 403 task_cancel_not_allowed。
      x-required-scope: video:create
      parameters:
        - $ref: "#/components/parameters/TaskId"
      responses:
        "403":
          description: 不支持取消任务
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/uploads/presign:
    post:
      tags: [Uploads]
      operationId: presignUpload
      summary: 获取素材上传预签名 URL
      x-required-scope: files:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, content_type]
              properties:
                filename:
                  type: string
                  maxLength: 255
                content_type:
                  type: string
                  enum:
                    - image/jpeg
                    - image/png
                    - image/webp
                    - video/mp4
                    - video/quicktime
                    - audio/mpeg
                    - audio/wav
                purpose:
                  type: string
                  enum: [reference, input]
                  default: reference
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PresignResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: av_live
      description: API Key from Developer Center

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 128
      description: 创建任务幂等键，24 小时内相同 body 重放，不同 body 返回 409
    TaskId:
      name: id
      in: path
      required: true
      schema:
        type: string
        example: vid_abc123

  headers:
    RequestId:
      schema:
        type: string
      description: 请求追踪 ID
    RateLimitLimit:
      schema:
        type: integer
    RateLimitRemaining:
      schema:
        type: integer
    RateLimitReset:
      schema:
        type: integer
      description: Unix 秒

  schemas:
    PublicModelId:
      type: string
      enum:
        - seedance-2.5-text-to-video
        - seedance-2.5-image-to-video
        - seedance-2.5-reference-to-video
        - seedance-2.0-text-to-video
        - seedance-2.0-image-to-video
        - seedance-2.0-reference-to-video
        - seedance-2.0-fast-text-to-video
        - seedance-2.0-fast-image-to-video
        - seedance-2.0-fast-reference-to-video
        - seedance-2.0-mini-text-to-video
        - seedance-2.0-mini-image-to-video
        - seedance-2.0-mini-reference-to-video

    Quality:
      type: string
      enum: ["480p", "720p", "1080p"]
      default: "720p"

    AspectRatio:
      type: string
      enum: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "adaptive"]
      default: adaptive

    TaskStatus:
      type: string
      enum: [queued, processing, succeeded, failed, cancelled]

    CreateGenerationRequest:
      type: object
      required: [model, prompt]
      properties:
        model:
          $ref: "#/components/schemas/PublicModelId"
        prompt:
          type: string
          minLength: 1
          maxLength: 10000
          description: 画面描述；参考模式推荐使用 @image1 / @video1 / @audio1
        image_urls:
          type: array
          maxItems: 30
          items:
            type: string
            format: uri
          description: |
            text 模式禁止；image 模式 1–2 张（1=首帧，2=首尾帧）；
            reference 模式 Seedance 2.0 为 0–9 张，Seedance 2.5 为 0–30 张。须为公网 HTTPS。
        video_urls:
          type: array
          maxItems: 10
          items:
            type: string
            format: uri
          description: 仅 reference；Seedance 2.0 为 0–3 段，Seedance 2.5 为 0–10 段
        audio_urls:
          type: array
          maxItems: 10
          items:
            type: string
            format: uri
          description: 仅 reference；Seedance 2.0 不可单独只传 audio，Seedance 2.5 支持纯音频
        duration:
          type: integer
          minimum: 4
          maximum: 30
          default: 5
        quality:
          $ref: "#/components/schemas/Quality"
        aspect_ratio:
          $ref: "#/components/schemas/AspectRatio"
        generate_audio:
          type: boolean
          default: true
        callback_url:
          type: string
          format: uri
          description: HTTPS 回调；任务终态触发
        metadata:
          type: object
          additionalProperties:
            type: string
            maxLength: 500
          description: 客户自定义键值，原样回传

    TaskInfo:
      type: object
      properties:
        can_cancel:
          type: boolean
          description: 始终为 false。提交后的任务不可取消。
        estimated_time:
          type: integer
          description: 预估秒数
        video_duration:
          type: integer

    Usage:
      type: object
      properties:
        billing_rule:
          type: string
          example: per_second
        credits_reserved:
          type: integer
        credits_settled:
          type: integer
          nullable: true
        credits_refunded:
          type: integer
          nullable: true

    TaskResult:
      type: object
      nullable: true
      properties:
        download_url:
          type: string
          format: uri
        thumbnail_url:
          type: string
          format: uri
          nullable: true
        last_frame_url:
          type: string
          format: uri
          nullable: true
        duration:
          type: integer
        resolution:
          type: string
        aspect_ratio:
          type: string

    TaskError:
      type: object
      nullable: true
      properties:
        code:
          type: string
        message:
          type: string

    Task:
      type: object
      required: [id, object, model, status, created]
      properties:
        id:
          type: string
          example: vid_abc123
        object:
          type: string
          enum: [video.generation.task]
        model:
          $ref: "#/components/schemas/PublicModelId"
        status:
          $ref: "#/components/schemas/TaskStatus"
        progress:
          type: integer
          minimum: 0
          maximum: 100
        created:
          type: integer
          description: Unix 秒
        completed:
          type: integer
          nullable: true
        task_info:
          $ref: "#/components/schemas/TaskInfo"
        usage:
          $ref: "#/components/schemas/Usage"
        result:
          $ref: "#/components/schemas/TaskResult"
        error:
          $ref: "#/components/schemas/TaskError"
        metadata:
          type: object
          additionalProperties:
            type: string
        request_id:
          type: string

    QuoteResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        request_id:
          type: string
        data:
          type: object
          properties:
            model:
              $ref: "#/components/schemas/PublicModelId"
            credits_to_hold:
              type: integer
            currency_note:
              type: string
              example: credits

    ModelsResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
        data:
          type: object
          properties:
            models:
              type: array
              items:
                type: object
                properties:
                  id:
                    $ref: "#/components/schemas/PublicModelId"
                  name:
                    type: string
                  mode:
                    type: string
                    enum: [text-to-video, image-to-video, reference-to-video]
                  tier:
                    type: string
                    enum: [standard, fast, mini]
                  durations:
                    type: array
                    items:
                      type: integer
                  aspect_ratios:
                    type: array
                    items:
                      type: string
                  qualities:
                    type: array
                    items:
                      type: string
                  capabilities:
                    type: object
                    properties:
                      text_to_video:
                        type: boolean
                      image_to_video:
                        type: boolean
                      reference_to_video:
                        type: boolean
                      generate_audio:
                        type: boolean
                      max_images:
                        type: integer
                      max_videos:
                        type: integer
                      max_audios:
                        type: integer

    BalanceResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
        data:
          type: object
          properties:
            available_credits:
              type: integer
            frozen_credits:
              type: integer

    TaskListResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
        data:
          type: object
          properties:
            tasks:
              type: array
              items:
                $ref: "#/components/schemas/Task"
            next_cursor:
              type: string
              nullable: true

    PresignResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
        data:
          type: object
          properties:
            upload_url:
              type: string
              format: uri
              description: PUT 上传地址
            public_url:
              type: string
              format: uri
              description: 上传成功后可用于 image_urls/video_urls
            expires_at:
              type: integer
              description: Unix 秒
            headers:
              type: object
              additionalProperties:
                type: string
              description: 上传时需附带的头（如 Content-Type）

    ErrorBody:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          enum: [false]
        request_id:
          type: string
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - invalid_api_key
                - insufficient_scope
                - insufficient_credits
                - rate_limit_exceeded
                - too_many_concurrent_jobs
                - invalid_request
                - unsupported_model
                - unsupported_capability
                - unsafe_input
                - idempotency_conflict
                - task_not_found
                - task_cancel_not_allowed
                - internal_error
            message:
              type: string
            details:
              type: object
              additionalProperties: true

  responses:
    Unauthorized:
      description: 未授权
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            success: false
            error:
              code: invalid_api_key
              message: Invalid API key
    Forbidden:
      description: 禁止
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    PaymentRequired:
      description: 积分不足
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            success: false
            error:
              code: insufficient_credits
              message: Insufficient credits
              details:
                required: 160
                available: 20
    BadRequest:
      description: 请求错误
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    Unprocessable:
      description: 业务规则不支持
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    Conflict:
      description: 冲突（幂等或不可取消）
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    NotFound:
      description: 未找到
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            success: false
            error:
              code: task_not_found
              message: Task not found
    RateLimited:
      description: 限流
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
