openapi: 3.1.0
info:
  title: SourcesData Media API
  version: 0.1.0
  summary: Unified image and asynchronous media generation API
  description: |
    SourcesData 媒体 API提供同步图片生成与编辑，以及异步图片和视频任务。
    图片始终使用 `/v1/images/*` 提交并通过 `async: true` 启用异步模式；视频使用 `POST /v1/videos`。
    所有异步任务和资源地址均使用 SourcesData 的公开标识，不暴露内部上游信息。
  contact:
    name: SourcesData Support
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://dance.sourcesdata.com
    description: Sources Dance
tags:
  - name: Images
    description: 图片生成、编辑和短期资源代理
  - name: Tasks
    description: 异步图片和视频任务
  - name: Videos
    description: 视频内容下载
security:
  - bearerAuth: []
paths:
  /v1/images/generations:
    post:
      tags: [Images]
      operationId: createImage
      summary: 生成图片
      description: |
        根据模型和提示词生成图片。默认同步返回图片资源；设置 `async: true` 时返回异步任务。
        `response_format=url` 返回短期 SourcesData 图片代理地址，`b64_json` 返回 Base64 内容。
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageGenerationRequest'
            examples:
              basic:
                summary: 基础图片生成
                value:
                  model: gpt-image-2
                  prompt: A clean studio photograph of a glass perfume bottle
                  n: 1
                  size: 1024x1024
                  quality: medium
                  response_format: url
      responses:
        '200':
          description: 同步图片结果、异步任务；`stream=true` 时返回 SSE 事件流
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ImageResponse'
                  - $ref: '#/components/schemas/MediaTask'
            text/event-stream:
              schema: { type: string }
              examples:
                completed:
                  value: |
                    event: image_generation.completed
                    data: {"type":"image_generation.completed","b64_json":"<base64>"}

                    data: [DONE]
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /v1/images/edits:
    post:
      tags: [Images]
      operationId: editImage
      summary: 编辑图片
      description: |
        使用一张或多张参考图片执行编辑。默认同步返回图片资源；设置 `async: true` 时返回异步任务。
        具体图片数量、格式和大小由模型能力决定。
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageEditJSONRequest'
            examples:
              referencedImages:
                summary: 使用本站可访问的图片引用进行编辑
                value:
                  model: gpt-image-2
                  prompt: Replace the background with a clean white studio
                  images:
                    - https://dance.sourcesdata.com/v1/images/proxy/token_example123
                  response_format: url
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImageEditRequest'
            encoding:
              image:
                contentType: image/png, image/jpeg, image/webp
      responses:
        '200':
          description: 同步图片结果、异步任务；`stream=true` 时返回 SSE 事件流
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ImageResponse'
                  - $ref: '#/components/schemas/MediaTask'
            text/event-stream:
              schema: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413':
          description: 上传内容超过限制
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /v1/videos:
    post:
      tags: [Tasks]
      operationId: createVideoTask
      summary: 创建异步视频任务
      description: |
        创建异步视频任务。异步图片仍从 `/v1/images/generations` 或 `/v1/images/edits`
        提交并设置 `async: true`。响应中的 `id` 和 `task_id` 均为 SourcesData 公共任务 ID。
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VideoCreateRequest' }
            examples:
              textToVideo:
                summary: 文生视频
                value:
                  model: seedance-2.0-fast
                  prompt: A paper boat drifting through a rain puddle
                  seconds: 4
                  size: 1280x720
          multipart/form-data:
            schema: { $ref: '#/components/schemas/VideoCreateMultipartRequest' }
            encoding:
              input_reference:
                contentType: image/png, image/jpeg, image/webp, video/mp4, video/quicktime, video/webm
      responses:
        '200':
          description: 已创建异步媒体任务
          headers:
            Retry-After:
              description: 建议的首次轮询等待秒数
              schema: { type: integer, minimum: 1 }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MediaTask' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /v1/videos/{task_id}:
    parameters:
      - $ref: '#/components/parameters/TaskId'
    get:
      tags: [Tasks]
      operationId: getMediaTask
      summary: 查询异步媒体任务
      description: |
        返回调用者拥有的任务快照。图片任务完成后从响应中的本站 `url` 或 `data[].url`
        下载结果；视频任务完成后通过 `/v1/videos/{task_id}/content` 下载。
      responses:
        '200':
          description: 当前任务状态
          headers:
            Retry-After:
              description: 非终态任务建议的下一次轮询等待秒数
              schema: { type: integer, minimum: 1 }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MediaTask' }
              examples:
                imageCompleted:
                  summary: 图片任务已完成
                  value:
                    id: task_image_example123
                    task_id: task_image_example123
                    object: image
                    model: gpt-image-2
                    status: completed
                    progress: 100
                    created_at: 1786579200
                    url: https://dance.sourcesdata.com/v1/images/proxy/token_example123
                    data:
                      - url: https://dance.sourcesdata.com/v1/images/proxy/token_example123
                videoProcessing:
                  summary: 视频任务处理中
                  value:
                    id: task_video_example123
                    task_id: task_video_example123
                    object: video
                    model: seedance-2.0-fast
                    status: in_progress
                    progress: 48
                    created_at: 1786579200
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/TaskNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
  /v1/videos/{task_id}/content:
    parameters:
      - $ref: '#/components/parameters/TaskId'
    get:
      tags: [Videos]
      operationId: downloadVideoContent
      summary: 下载视频内容
      description: 下载已完成任务的视频。支持 `Range` 断点续传，且必须通过任务所有者认证。
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Range'
      responses:
        '200':
          description: 完整视频内容
          headers:
            Accept-Ranges: { $ref: '#/components/headers/AcceptRanges' }
            Content-Length: { $ref: '#/components/headers/ContentLength' }
            Content-Type: { $ref: '#/components/headers/MediaContentType' }
          content:
            video/mp4:
              schema: { type: string, contentMediaType: video/mp4, contentEncoding: binary }
            application/octet-stream:
              schema: { type: string, contentEncoding: binary }
        '206':
          description: 部分视频内容
          headers:
            Accept-Ranges: { $ref: '#/components/headers/AcceptRanges' }
            Content-Range: { $ref: '#/components/headers/ContentRange' }
            Content-Length: { $ref: '#/components/headers/ContentLength' }
          content:
            video/mp4:
              schema: { type: string, contentMediaType: video/mp4, contentEncoding: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/TaskNotFound' }
        '409':
          description: 任务尚未完成
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '416':
          description: 请求的字节范围无效
          headers:
            Content-Range: { $ref: '#/components/headers/ContentRange' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/UpstreamError' }
    head:
      tags: [Videos]
      operationId: inspectVideoContent
      summary: 查询视频内容元数据
      description: 返回内容长度、类型和 Range 能力，不传输响应体。
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Range'
      responses:
        '200':
          description: 视频内容元数据
          headers:
            Accept-Ranges: { $ref: '#/components/headers/AcceptRanges' }
            Content-Length: { $ref: '#/components/headers/ContentLength' }
            Content-Type: { $ref: '#/components/headers/MediaContentType' }
        '206':
          description: 指定字节范围的视频内容元数据
          headers:
            Accept-Ranges: { $ref: '#/components/headers/AcceptRanges' }
            Content-Range: { $ref: '#/components/headers/ContentRange' }
            Content-Length: { $ref: '#/components/headers/ContentLength' }
            Content-Type: { $ref: '#/components/headers/MediaContentType' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/TaskNotFound' }
        '409':
          description: 任务尚未完成
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '416':
          description: 请求的字节范围无效
          headers:
            Content-Range: { $ref: '#/components/headers/ContentRange' }
        '500': { $ref: '#/components/responses/InternalError' }
  /v1/images/proxy/{token}:
    parameters:
      - name: token
        in: path
        required: true
        description: 图片响应中返回的短期加密资源 token
        schema: { type: string, minLength: 16 }
    get:
      tags: [Images]
      operationId: downloadImageProxy
      summary: 下载短期图片资源
      description: 使用图片响应中的短期签名 URL下载图片。该接口不需要额外 API Key。
      security: []
      responses:
        '200':
          description: 图片内容
          headers:
            Content-Length: { $ref: '#/components/headers/ContentLength' }
            Content-Type: { $ref: '#/components/headers/ImageContentType' }
          content:
            image/png: { schema: { type: string, contentMediaType: image/png, contentEncoding: binary } }
            image/jpeg: { schema: { type: string, contentMediaType: image/jpeg, contentEncoding: binary } }
            image/webp: { schema: { type: string, contentMediaType: image/webp, contentEncoding: binary } }
        '404':
          description: 资源不存在或 token 已过期
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/UpstreamError' }
    head:
      tags: [Images]
      operationId: inspectImageProxy
      summary: 查询短期图片元数据
      description: 返回图片内容长度和类型，不传输响应体。
      security: []
      responses:
        '200':
          description: 图片资源元数据
          headers:
            Content-Length: { $ref: '#/components/headers/ContentLength' }
            Content-Type: { $ref: '#/components/headers/ImageContentType' }
        '404':
          description: 资源不存在或 token 已过期
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500': { $ref: '#/components/responses/InternalError' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: Sources Dance API Key
  parameters:
    TaskId:
      name: task_id
      in: path
      required: true
      description: SourcesData 公共任务 ID
      schema: { type: string, pattern: '^task_[A-Za-z0-9_-]+$' }
      example: task_example123
    Range:
      name: Range
      in: header
      required: false
      description: HTTP 字节范围，例如 `bytes=0-1048575`
      schema: { type: string, pattern: '^bytes=[0-9]*-[0-9]*$' }
      example: bytes=0-1048575
  headers:
    AcceptRanges:
      description: 支持的 Range 单位
      schema: { type: string, const: bytes }
    ContentRange:
      description: 当前响应的字节范围或无效范围时的总长度
      schema: { type: string }
    ContentLength:
      description: 响应内容字节数
      schema: { type: integer, format: int64, minimum: 0 }
    MediaContentType:
      description: 视频 MIME 类型
      schema: { type: string, examples: [video/mp4] }
    ImageContentType:
      description: 图片 MIME 类型
      schema: { type: string, examples: [image/png] }
  responses:
    BadRequest:
      description: 参数无效或模型不支持该参数组合
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Unauthorized:
      description: API Key 缺失或无效
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    TaskNotFound:
      description: 任务不存在或不属于当前用户
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    RateLimited:
      description: 请求频率或额度受限
      headers:
        Retry-After:
          description: 建议等待秒数
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    InternalError:
      description: SourcesData 内部错误
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    UpstreamError:
      description: 媒体生成服务暂时不可用，响应不会包含内部上游信息
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
  schemas:
    ImageGenerationRequest:
      type: object
      additionalProperties: true
      required: [model, prompt]
      properties:
        model: { type: string, minLength: 1, examples: [gpt-image-2] }
        prompt: { type: string, minLength: 1, maxLength: 32000 }
        n: { type: integer, minimum: 1, maximum: 10, default: 1 }
        size: { type: string, examples: [1024x1024] }
        aspect_ratio: { type: string, examples: ['1:1'] }
        output_resolution: { type: string, examples: [1K, 2K, 4K] }
        quality: { type: string, examples: [medium] }
        response_format: { type: string, enum: [url, b64_json], default: url }
        async:
          type: boolean
          default: false
          description: 为 true 时立即返回 SourcesData 异步任务。
        user: { type: string, maxLength: 128 }
        stream:
          type: boolean
          default: false
          description: 为 true 时以 SSE 返回图片事件和终止标记。
        partial_images:
          type: integer
          minimum: 0
          maximum: 3
          description: 流式请求中希望接收的预览图片数量。
    ImageEditRequest:
      type: object
      additionalProperties: true
      required: [model, prompt, image]
      properties:
        model: { type: string, minLength: 1, examples: [gpt-image-2] }
        prompt: { type: string, minLength: 1, maxLength: 32000 }
        image:
          oneOf:
            - { type: string, format: binary }
            - { type: array, items: { type: string, format: binary }, minItems: 1 }
        mask: { type: string, format: binary }
        n: { type: integer, minimum: 1, maximum: 10, default: 1 }
        size: { type: string, examples: [1024x1024] }
        quality: { type: string, examples: [medium] }
        response_format: { type: string, enum: [url, b64_json], default: url }
        async: { type: boolean, default: false }
        stream: { type: boolean, default: false }
        partial_images: { type: integer, minimum: 0, maximum: 3 }
    ImageEditJSONRequest:
      type: object
      additionalProperties: true
      required: [model, prompt]
      anyOf:
        - required: [image]
        - required: [images]
      properties:
        model: { type: string, minLength: 1, examples: [gpt-image-2] }
        prompt: { type: string, minLength: 1, maxLength: 32000 }
        image:
          type: string
          minLength: 1
          description: HTTPS URL 或 data URL 图片引用
        images:
          type: array
          minItems: 1
          items: { type: string, minLength: 1 }
          description: 一张或多张 HTTPS URL 或 data URL 图片引用
        mask:
          type: string
          minLength: 1
          description: 可选的 HTTPS URL 或 data URL 遮罩
        n: { type: integer, minimum: 1, maximum: 10, default: 1 }
        size: { type: string, examples: [1024x1024] }
        quality: { type: string, examples: [medium] }
        response_format: { type: string, enum: [url, b64_json], default: url }
        async: { type: boolean, default: false }
        stream: { type: boolean, default: false }
        partial_images: { type: integer, minimum: 0, maximum: 3 }
    VideoCreateRequest:
      type: object
      additionalProperties: true
      required: [model, prompt]
      properties:
        model: { type: string, minLength: 1, examples: [seedance-2.0-fast] }
        prompt: { type: string, minLength: 1, maxLength: 32000 }
        seconds:
          oneOf:
            - { type: integer, minimum: 1, maximum: 120 }
            - { type: string, pattern: '^[0-9]+$' }
        duration: { type: integer, minimum: 1, maximum: 120 }
        size: { type: string, examples: [1280x720] }
        resolution: { type: string, examples: [720p] }
        aspect_ratio: { type: string, examples: ['16:9'] }
        image_url: { type: string, format: uri }
        input_reference:
          oneOf:
            - { type: string, format: uri }
            - { type: array, items: { type: string, format: uri }, minItems: 1 }
        audio: { type: boolean, default: false }
    VideoCreateMultipartRequest:
      allOf:
        - $ref: '#/components/schemas/VideoCreateRequest'
        - type: object
          properties:
            input_reference:
              oneOf:
                - { type: string, format: binary }
                - { type: array, items: { type: string, format: binary }, minItems: 1 }
    ImageResponse:
      type: object
      required: [created, data]
      properties:
        created: { type: integer, format: int64 }
        data:
          type: array
          minItems: 1
          items:
            oneOf:
              - $ref: '#/components/schemas/ImageURLResult'
              - $ref: '#/components/schemas/ImageBase64Result'
    ImageURLResult:
      type: object
      additionalProperties: false
      required: [url]
      properties:
        url:
          type: string
          format: uri
          description: 短期有效的 SourcesData 图片代理 URL
        revised_prompt: { type: string }
    ImageBase64Result:
      type: object
      additionalProperties: false
      required: [b64_json]
      properties:
        b64_json: { type: string, contentEncoding: base64 }
        revised_prompt: { type: string }
    MediaTask:
      type: object
      additionalProperties: false
      required: [id, task_id, object, model, status, progress, created_at]
      properties:
        id:
          type: string
          pattern: '^task_[A-Za-z0-9_-]+$'
          description: SourcesData 公共任务 ID
        task_id:
          type: string
          pattern: '^task_[A-Za-z0-9_-]+$'
          description: 与 `id` 相同的兼容字段
        object:
          type: string
          enum: [image, video]
          description: 任务输出类型
        model: { type: string }
        status:
          type: string
          enum: [queued, in_progress, completed, failed]
        progress: { type: integer, minimum: 0, maximum: 100 }
        created_at: { type: integer, format: int64 }
        completed_at: { type: integer, format: int64 }
        url:
          type: string
          format: uri
          description: 图片任务完成后返回的短期 SourcesData 图片代理 URL；视频任务不返回该字段。
        data:
          type: array
          description: 图片任务完成后的结果列表；视频任务不返回该字段。
          minItems: 1
          items:
            oneOf:
              - $ref: '#/components/schemas/ImageURLResult'
              - $ref: '#/components/schemas/ImageBase64Result'
        error:
          $ref: '#/components/schemas/TaskError'
      example:
        id: task_example123
        task_id: task_example123
        object: video
        model: seedance-2.0-fast
        status: queued
        progress: 0
        created_at: 1786579200
    TaskError:
      type: object
      additionalProperties: false
      required: [message, code]
      properties:
        message: { type: string }
        code: { type: string }
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [message, type]
          properties:
            message: { type: string }
            type: { type: string }
            code: { type: string }
            request_id: { type: string }
      example:
        error:
          message: The request could not be completed.
          type: upstream_error
          code: media_generation_failed
          request_id: req_example123
