{
  "openapi": "3.1.0",
  "info": {
    "title": "YukkuriGen AI-operated API",
    "version": "1.0.0",
    "description": "AI（Claude / Cursor / スクリプト）から YukkuriGen を操作し、台本から .ymmp（YMM4プロジェクト）や MP4 を生成する REST API。MCP でも同じ機能を提供します（POST /api/mcp）。"
  },
  "servers": [
    {
      "url": "https://app.yukkurigen.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key: Authorization: Bearer yg_live_...  (発行: /settings/api-keys)"
      }
    },
    "schemas": {
      "ScriptLine": {
        "type": "object",
        "required": [
          "speaker",
          "text"
        ],
        "properties": {
          "speaker": {
            "type": "string",
            "description": "キャラクターid（reimu, marisa, zundamon 等。/api/v1/characters 参照）"
          },
          "text": {
            "type": "string",
            "description": "セリフ本文"
          },
          "reading": {
            "type": "string",
            "description": "発音かな（未指定なら本文を読む）"
          },
          "emotion": {
            "type": "string",
            "description": "表情（任意）"
          },
          "cameraMode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "dynamic",
              "summary",
              null
            ],
            "description": "その行のカメラ。dynamic=背景ごと話者に寄る / summary=立ち絵を消す / 省略・null=通常。盛り上がり・オチで2〜4回だけ使うこと（全行 dynamic だと寄りが効かなくなる）。MP4 レンダーにのみ効き、.ymmp には現れない。"
          }
        }
      },
      "ExportResult": {
        "type": "object",
        "properties": {
          "exportId": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "ymmp"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "completed"
            ]
          },
          "downloadUrl": {
            "type": "string",
            "format": "uri",
            "description": "24時間有効の署名付きURL"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "unauthenticated",
              "insufficient_scope",
              "forbidden",
              "plan_required",
              "insufficient_credits",
              "rate_limited",
              "validation_error",
              "not_found",
              "line_not_found",
              "duplicate_index",
              "unknown_speaker",
              "missing_audio",
              "cast_mismatch",
              "price_unavailable",
              "export_failed",
              "progress_unavailable",
              "render_start_failed",
              "render_incomplete",
              "internal_error",
              "in_flight",
              "unavailable"
            ],
            "description": "エラー種別。progress_unavailable は進捗が読めなかっただけで、レンダー自体は継続している可能性がある（打ち切らないこと）。"
          },
          "purchaseUrl": {
            "type": "string",
            "format": "uri"
          },
          "retryAfter": {
            "type": "integer",
            "description": "429 のとき。再試行までの秒数（Retry-After ヘッダと同値）。"
          },
          "validSpeakers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "未知の speaker / キャラid を送ったとき。使用可能なキャラid の一覧。"
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/agent/generate": {
      "post": {
        "summary": "台本 → .ymmp を1コールで生成",
        "description": "3クレジット消費。API keyの場合 apiAccess（有料プラン〈スタンダード以上〉or 買い切り）が必要。",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "script"
                ],
                "properties": {
                  "output": {
                    "type": "string",
                    "enum": [
                      "ymmp"
                    ],
                    "default": "ymmp"
                  },
                  "title": {
                    "type": "string"
                  },
                  "templateId": {
                    "type": "string"
                  },
                  "fps": {
                    "type": "integer",
                    "enum": [
                      30,
                      60
                    ]
                  },
                  "script": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 500,
                    "items": {
                      "$ref": "#/components/schemas/ScriptLine",
                      "properties": {
                        "backgroundImageUrl": {
                          "type": "string",
                          "description": "この行から背景を差し替える（場面転換）。以降の行へ引き継がれる。"
                        }
                      }
                    }
                  },
                  "tachieDirs": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "キャラid → ローカル立ち絵フォルダ（眉/目/口/体 のパーツ方式）または 1枚画像。1枚画像を指定した場合のみ tachieFaces による表情切替が効く。（未指定なら音声＋字幕のみ）。素材は配布せず参照のみ。（絶対パスのみ有効: C:\\... などのドライブパスかUNC。URLやサイト絶対パスは無視される）"
                  },
                  "bgmPath": {
                    "type": "string",
                    "description": "BGMに使うローカル音声ファイルのパス（任意）。（絶対パスのみ有効: C:\\... などのドライブパスかUNC。URLやサイト絶対パスは無視される）"
                  },
                  "tachieFaces": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "description": "キャラid → 表情 → ローカル顔画像。行の emotion に対応する顔へ切り替える。（絶対パスのみ有効: C:\\... などのドライブパスかUNC。URLやサイト絶対パスは無視される）（立ち絵に「1枚画像」を指定したキャラにのみ有効。フォルダ指定=パーツ方式のキャラでは無視される）"
                  },
                  "tachieParts": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "properties": {
                        "eyebrow": {
                          "type": "string"
                        },
                        "eye": {
                          "type": "string"
                        },
                        "mouth": {
                          "type": "string"
                        },
                        "body": {
                          "type": "string"
                        },
                        "hair": {
                          "type": "string"
                        },
                        "complexion": {
                          "type": "string"
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "キャラid → 立ち絵パーツ画像の明示パス。キーは eyebrow/eye/mouth/body/hair/complexion のみ（他は400）。未指定は <フォルダ>/<部位>/00.png を仮定。"
                  },
                  "channelId": {
                    "type": "string",
                    "description": "チャンネルid（省略可。テンプレートの既定を引く）"
                  },
                  "backgroundImageUrl": {
                    "type": "string",
                    "description": "動画全体の背景画像/動画のURL（https のみ、内部アドレス不可）。省略すると無地の背景になり、画面の大半が空いた動画になる。"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "生成成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportResult"
                }
              }
            }
          },
          "400": {
            "description": "リクエスト不正（code: validation_error。未知の speaker / キャラid のときは validSpeakers 同梱）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "認証失敗",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "権限またはクレジット不足（code: insufficient_credits または plan_required、purchaseUrl 同梱）。この鍵は解放されるので、購入・アップグレード後は同じ鍵でそのまま再試行してよい。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "同じ Idempotency-Key の処理が進行中（code: in_flight）。少し待って同じ鍵で再試行する。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "レート上限（code: rate_limited、retryAfter 同梱）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "生成に失敗（code: export_failed）。予約したクレジットは返金済み。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "冪等キーの判定ができなかった場合は鍵が取られておらず課金も起きていないので、同じ鍵でそのまま再試行する。クレジット確保に失敗した場合は課金の成否が確定しておらず鍵は保持されるので、GET /api/v1/credits で残高を確認し、しばらく置いてから同じ鍵で再試行する（鍵は65分で解ける）。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "同じ鍵で再投入すると最初の成功応答が返り、課金は起きない（24時間）。処理中なら 409 code:\"in_flight\"。"
          }
        ]
      }
    },
    "/api/v1/projects/{projectId}/export/ymmp": {
      "post": {
        "summary": "既存プロジェクトを .ymmp 書き出し",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "同じ鍵で再投入すると最初の成功応答が返り、課金は起きない（24時間）。処理中なら 409 code:\"in_flight\"。"
          }
        ],
        "responses": {
          "200": {
            "description": "成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportResult"
                }
              }
            }
          },
          "400": {
            "description": "リクエスト不正（code: validation_error。未知のキャラidのときは validSpeakers 同梱）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "権限またはクレジット不足（code: insufficient_credits または plan_required、purchaseUrl 同梱）。この鍵は解放されるので、購入・アップグレード後は同じ鍵でそのまま再試行してよい。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "プロジェクトが無い（code: not_found）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "同じ Idempotency-Key の処理が進行中（code: in_flight）。少し待って同じ鍵で再試行する。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "生成に失敗（code: export_failed）。予約したクレジットは返金済み。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "冪等キーの判定ができなかった場合は鍵が取られておらず課金も起きていないので、同じ鍵でそのまま再試行する。クレジット確保に失敗した場合は課金の成否が確定しておらず鍵は保持されるので、GET /api/v1/credits で残高を確認し、しばらく置いてから同じ鍵で再試行する（鍵は65分で解ける）。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tachieDirs": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "キャラid → ローカル立ち絵フォルダ（眉/目/口/体 のパーツ方式）または 1枚画像。1枚画像を指定した場合のみ tachieFaces による表情切替が効く。（絶対パスのみ有効: C:\\... などのドライブパスかUNC。URLやサイト絶対パスは無視される）"
                  },
                  "fps": {
                    "type": "integer",
                    "enum": [
                      30,
                      60
                    ]
                  },
                  "bgmPath": {
                    "type": "string",
                    "description": "BGMに使うローカル音声ファイルのパス（任意）。（絶対パスのみ有効: C:\\... などのドライブパスかUNC。URLやサイト絶対パスは無視される）"
                  },
                  "tachieFaces": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "description": "キャラid → 表情 → ローカル顔画像。行の emotion に対応する顔へ切り替える。（絶対パスのみ有効: C:\\... などのドライブパスかUNC。URLやサイト絶対パスは無視される）（立ち絵に「1枚画像」を指定したキャラにのみ有効。フォルダ指定=パーツ方式のキャラでは無視される）"
                  },
                  "tachieParts": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "properties": {
                        "eyebrow": {
                          "type": "string"
                        },
                        "eye": {
                          "type": "string"
                        },
                        "mouth": {
                          "type": "string"
                        },
                        "body": {
                          "type": "string"
                        },
                        "hair": {
                          "type": "string"
                        },
                        "complexion": {
                          "type": "string"
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "キャラid → 立ち絵パーツ画像の明示パス。キーは eyebrow/eye/mouth/body/hair/complexion のみ（他は400）。未指定は <フォルダ>/<部位>/00.png を仮定。"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/exports/{exportId}": {
      "get": {
        "summary": "書き出し状態 + 新しいダウンロードURL",
        "parameters": [
          {
            "name": "exportId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "状態",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exportId": {
                      "type": "string"
                    },
                    "projectId": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "processing",
                        "completed",
                        "failed"
                      ]
                    },
                    "downloadUrl": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri"
                    },
                    "manifest": {
                      "type": [
                        "object",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "無し",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{projectId}/render": {
      "post": {
        "summary": "MP4 レンダーを開始（Remotion Lambda・非同期・5cr）",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "同じ鍵で再投入すると最初の成功応答が返り、課金は起きない（24時間）。処理中なら 409 code:\"in_flight\"。"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "outputFormat": {
                    "type": "string",
                    "enum": [
                      "landscape",
                      "portrait"
                    ],
                    "default": "landscape"
                  },
                  "platform": {
                    "type": "string",
                    "enum": [
                      "youtube",
                      "tiktok",
                      "shorts"
                    ],
                    "default": "youtube"
                  },
                  "codec": {
                    "type": "string",
                    "enum": [
                      "h264",
                      "vp8",
                      "vp9"
                    ],
                    "default": "h264"
                  },
                  "waitForCompletion": {
                    "type": "boolean",
                    "default": false,
                    "description": "true で完了まで待つ（最大16分リクエストを保持する）。false なら renderId を返し progress で追う。AI からは false を推奨。"
                  },
                  "fontSize": {
                    "type": "number"
                  },
                  "titleFontSize": {
                    "type": "number"
                  },
                  "voicePlaybackRate": {
                    "type": "number",
                    "minimum": 0.5,
                    "maximum": 2
                  },
                  "bgmVolume": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "showOutroCard": {
                    "type": "boolean"
                  },
                  "materialImageFilter": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "blur",
                        "top-dark"
                      ]
                    },
                    "description": "素材画像にかけるフィルタ。未指定ならプロジェクトの設定を使う"
                  },
                  "title": {
                    "type": "string",
                    "description": "動画内に出すタイトル（プロジェクト名と別にしたいとき）"
                  },
                  "preview": {
                    "type": "object",
                    "description": "確認用の安いプレビュー。1クレジット・低解像度・指定行の周辺だけ（上限あり）。全編は焼けない。",
                    "properties": {
                      "fromLine": {
                        "type": "integer",
                        "description": "開始行"
                      },
                      "toLine": {
                        "type": "integer",
                        "description": "終了行（省略時は fromLine の1行）"
                      }
                    },
                    "required": [
                      "fromLine"
                    ]
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "開始。waitForCompletion:true でも、レンダーが始まったあとに完了確認が失敗した場合は status:\"rendering\" と renderId を返す（\"completed\" とは限らない）。その場合の課金は正当で、進捗（GET .../render/{renderId}/progress）で完了を追える。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "renderId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "rendering",
                        "completed"
                      ]
                    },
                    "outputUrl": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "リクエスト不正（code: validation_error）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "権限またはクレジット不足（code: insufficient_credits または plan_required、purchaseUrl 同梱）。この鍵は解放されるので、購入・アップグレード後は同じ鍵でそのまま再試行してよい。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "プロジェクトが無い（code: not_found）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "同じ Idempotency-Key の処理が進行中（code: in_flight）。少し待って同じ鍵で再試行する。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "レート上限（code: rate_limited、retryAfter 同梱）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "レンダー開始に失敗（code: render_start_failed）、または waitForCompletion で待機中に完了を確認できなかった（code: render_incomplete、renderId 同梱。レンダーは開始済みなのでそのまま再投入せず、進捗を先に確認すること）。render_start_failed では予約分を返金済み、または返金待ちとして記録済みで後追いの掃除処理が戻すため、応答時点の残高にはまだ反映されていないことがある（返金と記録の両方の書き込みが落ちた場合はサーバログにのみ残る）。いずれの場合も残高は GET /api/v1/credits で確認できる。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "冪等キーの判定ができなかった場合は鍵が取られておらず課金も起きていないので、同じ鍵でそのまま再試行する。クレジット確保に失敗した場合は課金の成否が確定しておらず鍵は保持されるので、GET /api/v1/credits で残高を確認し、しばらく置いてから同じ鍵で再試行する（鍵は65分で解ける）。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{projectId}/render/{renderId}/progress": {
      "get": {
        "summary": "MP4 レンダー進捗＋出力URL",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "進捗",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "done": {
                      "type": "boolean",
                      "description": "レンダーが終了したか。**成功とは限らない** — 失敗（チャンク欠落、および後追いで確定した分）でも true になる。必ず失敗判定を先に行うこと。"
                    },
                    "overallProgress": {
                      "type": "number"
                    },
                    "outputFile": {
                      "type": "string",
                      "nullable": true,
                      "description": "成功時のみ非空。成功条件は done===true かつ outputFile が非空。"
                    },
                    "errors": {
                      "type": "array",
                      "nullable": true,
                      "description": "Lambda 側のエラー。リトライ予定のものも含むので、失敗判定には使わないこと。",
                      "items": {
                        "type": "object",
                        "properties": {
                          "message": {
                            "type": "string"
                          },
                          "isFatal": {
                            "type": "boolean"
                          },
                          "willRetry": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "fatalErrorEncountered": {
                      "type": "boolean",
                      "description": "true なら失敗が確定（リトライ不能）。done より先にこれを見ること。"
                    },
                    "renderId": {
                      "type": "string",
                      "description": "このレンダーのID（リクエストの renderId と同じ）。"
                    }
                  },
                  "description": "成功は done===true かつ outputFile が非空。失敗は fatalErrorEncountered===true、または done===true なのに outputFile が空。失敗でも done が立つため、必ず失敗判定を先に行うこと。"
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "APIキー利用に有料プランが必要（code: plan_required、purchaseUrl 同梱）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "対象が無い（code: not_found）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "ポーリング頻度の上限（code: rate_limited）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "進捗の取得に失敗（code: progress_unavailable）。レンダーは継続している可能性があるため、打ち切らず再試行すること。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/characters": {
      "get": {
        "summary": "利用可能なゆっくりキャラクター一覧",
        "responses": {
          "200": {
            "description": "キャラ一覧"
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/credits": {
      "get": {
        "summary": "クレジット残高",
        "responses": {
          "200": {
            "description": "残高",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "number"
                    },
                    "monthly": {
                      "type": "number"
                    },
                    "monthlyUsed": {
                      "type": "number"
                    },
                    "extra": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/api-keys": {
      "post": {
        "summary": "APIキー発行（Web / IDトークンのみ）",
        "responses": {
          "201": {
            "description": "発行（平文は一度だけ返る）"
          },
          "400": {
            "description": "リクエスト不正（code: validation_error）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "summary": "APIキー一覧",
        "responses": {
          "200": {
            "description": "一覧"
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ymm4-assets": {
      "get": {
        "summary": "保存済みのYMM4素材パスを取得",
        "responses": {
          "200": {
            "description": "素材パス",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tachieDirs": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "description": "キャラid → ローカル立ち絵フォルダ or 画像"
                    },
                    "bgmPath": {
                      "type": "string",
                      "description": "ローカルBGMファイル"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "YMM4素材パスを保存（全置換。送らなかったキーは消える）",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tachieDirs": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "キャラid → ローカル立ち絵フォルダ or 画像"
                  },
                  "bgmPath": {
                    "type": "string",
                    "description": "ローカルBGMファイル"
                  },
                  "tachieFaces": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "description": "キャラid → 表情 → ローカル顔画像。行の emotion に対応する顔へ切り替える。"
                  },
                  "tachieParts": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "properties": {
                        "eyebrow": {
                          "type": "string"
                        },
                        "eye": {
                          "type": "string"
                        },
                        "mouth": {
                          "type": "string"
                        },
                        "body": {
                          "type": "string"
                        },
                        "hair": {
                          "type": "string"
                        },
                        "complexion": {
                          "type": "string"
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "キャラid → 立ち絵パーツ画像の明示パス。キーは eyebrow/eye/mouth/body/hair/complexion のみ（他は400）。未指定は <フォルダ>/<部位>/00.png を仮定。"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "保存完了"
          },
          "400": {
            "description": "リクエスト不正（code: validation_error。未知のキャラidのときは validSpeakers 同梱）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/renders": {
      "get": {
        "summary": "全プロジェクトの書き出し一覧（打ち切ったあと結果を拾い直す用）",
        "responses": {
          "200": {
            "description": "レンダー一覧。返金済みのものは status=failed / outputUrl 空で返る。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "renders": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "projectId": {
                            "type": "string"
                          },
                          "projectTitle": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "completed",
                              "failed",
                              "processing"
                            ]
                          },
                          "outputUrl": {
                            "type": "string",
                            "description": "成功時のみ非空。返金済みなら空。"
                          },
                          "platform": {
                            "type": "string"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope / forbidden）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "サーバ内部エラー（code: internal_error）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/exports": {
      "get": {
        "summary": "書き出し一覧（応答を落としたあと exportId を拾い直す用）",
        "parameters": [
          {
            "name": "projectId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "指定するとそのプロジェクトの分だけ返す"
          }
        ],
        "responses": {
          "200": {
            "description": "書き出し一覧。返金済みのものは status=failed で返る。ダウンロードURLは GET /api/v1/exports/{exportId} で取得する。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exports": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "exportId": {
                            "type": "string"
                          },
                          "projectId": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "queued",
                              "processing",
                              "completed",
                              "failed"
                            ]
                          },
                          "creditCost": {
                            "type": "number"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "権限不足（code: insufficient_scope。projects:read が必要）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "サーバ内部エラー（code: internal_error）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{projectId}/lines": {
      "patch": {
        "summary": "指定した行だけ差し替える（update_lines）",
        "description": "行番号を指定して、その行だけを直す。触らない行は書かない。text / speaker / reading を変えた行は、その行の音声が無効化され audioInvalidated に載る——レンダー前に音声を作り直すこと。",
        "operationId": "updateLines",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "プロジェクトID"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "edits"
                ],
                "properties": {
                  "edits": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 200,
                    "description": "直す行だけを並べる。同じ index を2回書くと 400。",
                    "items": {
                      "type": "object",
                      "required": [
                        "index"
                      ],
                      "properties": {
                        "index": {
                          "type": "integer",
                          "description": "行番号（get_project が返す index）"
                        },
                        "text": {
                          "type": "string",
                          "description": "新しいセリフ本文"
                        },
                        "speaker": {
                          "type": "string",
                          "description": "話者id（/api/v1/characters 参照）"
                        },
                        "emotion": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "表情"
                        },
                        "reading": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "読み仮名"
                        },
                        "cameraMode": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "dynamic",
                            "summary",
                            null
                          ],
                          "description": "その行のカメラ。dynamic=背景ごと話者に寄る / summary=立ち絵を消す / 省略・null=通常。盛り上がり・オチで2〜4回だけ使うこと（全行 dynamic だと寄りが効かなくなる）。MP4 レンダーにのみ効き、.ymmp には現れない。"
                        },
                        "backgroundImageUrl": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "この行から背景を差し替える（場面転換）。null でこの行の指定を外す。https のみ。"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "更新できた",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "projectId": {
                      "type": "string"
                    },
                    "updated": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      },
                      "description": "実際に書き換えた行番号"
                    },
                    "lineCount": {
                      "type": "integer"
                    },
                    "audioInvalidated": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      },
                      "description": "音声が無効になった行番号。レンダー前に generate_audio が要る。"
                    },
                    "nextStep": {
                      "type": "string",
                      "description": "audioInvalidated が空でないときだけ付く"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "リクエスト不正（code: validation_error / duplicate_index / unknown_speaker）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "未認証（code: unauthenticated）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "プロジェクトまたは行が無い（code: not_found）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}
