openapi: 3.1.0
info:
  title: Pano Studio API
  version: 0.1.0
  description: |
    720° 全景图生成 API。异步任务模型：提交返回 202，轮询或 webhook 获取结果。

    认证：`Authorization: Bearer <api_key>`。
    幂等：同一个 `Idempotency-Key` 在 24 小时内重复提交，返回同一个任务。
servers:
  - url: https://api.pano.studio/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local
security:
  - bearerAuth: []
paths:
  /generations:
    post:
      summary: 创建一次全景生成
      operationId: createGeneration
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string, maxLength: 64 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SceneSpec" }
      responses:
        "202":
          description: 任务已入队
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Generation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /generations/{id}:
    get:
      summary: 查询任务状态
      operationId: getGeneration
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: 任务状态
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Generation" }
        "404": { $ref: "#/components/responses/NotFound" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 平台签发的 API Key，形如 `pk_live_xxx`
  responses:
    Unauthorized:
      description: 缺少或无效的 API Key
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    ValidationFailed:
      description: 参数校验失败
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    RateLimited:
      description: 超出限流
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    NotFound:
      description: 资源不存在
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
  schemas:
    SceneSpec:
      type: object
      required: [space, style]
      properties:
        projection:
          type: string
          const: equirectangular
          default: equirectangular
        space:
          type: object
          required: [type, areaSqm]
          properties:
            type:
              type: string
              enum: [living_room, exhibition_hall, restaurant, hotel_lobby, retail_store, scenic_spot, office, other]
            areaSqm: { type: number, minimum: 5, maximum: 20000 }
            ceilingM: { type: number, default: 3 }
            layout: { type: string, maxLength: 200 }
        camera:
          type: object
          properties:
            heightM: { type: number, default: 1.55 }
            headingDeg: { type: number, default: 0 }
            pitchDeg: { type: number, default: 0 }
        style:
          type: object
          required: [name]
          properties:
            name: { type: string, maxLength: 40 }
            palette: { type: array, items: { type: string } }
            materials: { type: array, items: { type: string } }
        lighting:
          type: object
          properties:
            time: { type: string, enum: [dawn, noon, dusk, night], default: dusk }
            weather: { type: string, enum: [clear, overcast, rain, snow], default: clear }
            sources: { type: array, items: { type: string } }
        content:
          type: object
          properties:
            furniture: { type: array, items: { type: string } }
            people: { type: integer, minimum: 0, maximum: 10, default: 0 }
            brand: { type: [string, "null"] }
        constraints:
          type: object
          properties:
            avoid: { type: array, items: { type: string } }
            seam: { type: string, enum: [left-right-blend, none], default: left-right-blend }
        output:
          type: object
          properties:
            aspect: { type: string, const: "2:1" }
            resolution: { type: string, enum: ["4096x2048", "8192x4096", "16384x8192"], default: 8192x4096 }
            format: { type: string, enum: [jpg, png, webp], default: jpg }
    Generation:
      type: object
      properties:
        id: { type: string, example: gen_9f2c4b7a1e }
        status:
          type: string
          enum: [queued, running, scoring, upscaling, postprocessing, succeeded, failed, canceled]
        createdAt: { type: string, format: date-time }
        estimatedSeconds: { type: integer }
        progress: { type: number, minimum: 0, maximum: 1 }
        candidates:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              score: { type: number }
              url: { type: string, format: uri }
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
    Problem:
      type: object
      required: [type, title, status]
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        requestId: { type: string }
