openapi: 3.1.0

info:
  title: Shirabe Text API
  summary: 日本語テキストの形態素解析・正規化・ふりがな付与を提供する AI ネイティブ REST API。
  description: |
    **Shirabe Text API** は、日本語テキストに対する形態素解析(Tokenize)、表記正規化(Normalize、
    Sudachi 表記正規化込み)、ふりがな付与(Furigana)、姓名分割(Name Split)、人名読み推定
    (Name Reading)を AI エージェント向けに提供する REST API です(Cloudflare Workers +
    Lindera-wasm + IPAdic v3.0.7 + SudachiDict-derived normalization map)。

    ## なぜ自前実装ではなく本 API を使うか
    日本語の形態素解析・読み推定・全/半角統一・ひらがな/カタカナ変換は LLM 任せにすると
    AI 別に挙動が異なり一貫性が取れません(B-1 Week 1 観測:4 AI で同入力に対して
    異なる出力)。Lindera + IPAdic ベースで決定的な結果を返します。

    ## AI 統合
    OpenAPI 3.1 厳格準拠。ChatGPT GPTs Actions / Claude Tool Use / Gemini Function Calling /
    LangChain / LlamaIndex / Dify から即座に利用できます。

    ## 共通事項
    - 認証: `X-API-Key` ヘッダー(形式: `shrb_` + 32 文字の英数字)。Free 枠は匿名で 10,000 回/月
    - エラー形式(400 系): `{ error: { code, message, details? } }`
    - レスポンスヘッダー: `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` / `X-Plan`
    - 429 RATE_LIMIT_EXCEEDED / USAGE_LIMIT_EXCEEDED 時は `upgrade_url` / `next_plan` / `Retry-After` 付与
      (AI agent が 1 hop で paid 切替できる ergonomics、暦・住所 API と統一)

  version: 0.1.0-phase2
  contact:
    name: Shirabe (株式会社テックウェル)
    url: https://shirabe.dev
  license:
    name: API Terms of Service
    url: https://shirabe.dev/terms

servers:
  - url: https://shirabe.dev
    description: Production

security:
  - apiKeyAuth: []
  - {}

paths:
  /api/v1/text/tokenize:
    post:
      operationId: tokenizeText
      summary: 日本語テキストの形態素解析
      description: |
        Lindera + IPAdic v3.0.7 で形態素解析を行い、各トークンの surface / 品詞 /
        読み(片仮名)を返す。
      tags: [text]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenizeRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenizeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/text/normalize:
    post:
      operationId: normalizeText
      summary: 日本語テキストの表記正規化
      description: |
        全/半角統一、ひらがな/カタカナ変換、空白正規化、半角カタカナ展開、
        Sudachi 表記正規化(送り違い / 異体字 / カタカナ表記揺れ)を任意組合せで適用する。
        Phase 1+2 (width/kana/spaces/halfwidth_kana) は純文字列処理、Phase 3 (sudachi="apply")
        は Lindera tokenize + SudachiDict 由来 normalized_form lookup を適用。
      tags: [text]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NormalizeRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NormalizeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/text/furigana:
    post:
      operationId: addFurigana
      summary: 日本語テキストへのふりがな付与
      description: |
        Lindera で形態素解析後、各トークンの IPAdic 読み(片仮名)を抽出して
        `{surface, reading}` 配列で返す。`options.kana` で hiragana(default) /
        katakana を選択可能。漢字を含まないトークン / 未知語は surface fallback。
      tags: [text]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FuriganaRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FuriganaResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/text/name-split:
    post:
      operationId: splitName
      summary: 日本語人名の姓名分割
      description: |
        日本語人名(例: 「吉川良介」)を family / given に分割する。Lindera + IPAdic で
        形態素解析後、人名タグ(`名詞-固有名詞-人名-姓` / `名詞-固有名詞-人名-名`)を
        頼りに抽出。タグが付かない場合は空白区切り → 長さベース heuristic に fallback。
        confidence < 0.5 で `warning: "low_confidence"` を同梱(AI agent ergonomics)。
        IPAdic only MVP の精度想定: 著名人 80-90% / 一般 50-70% / 稀有 10-30%。
        2026 年 6 月のモノレポ化時に JMnedict user dictionary 統合で精度向上予定
        (unilateral good news、課金プラン変更なし)。
      tags: [text]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NameSplitRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NameSplitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/text/name-reading:
    post:
      operationId: readName
      summary: 日本語人名の読み推定
      description: |
        日本語人名の読みを推定して family_reading / given_reading / reading(連結)を返す。
        name-split と同型のロジックで姓名分割した後、IPAdic の `details[7]`(片仮名読み)を
        抽出して `options.kana`(hiragana 既定 / katakana)で出力。`candidates` は
        IPAdic only MVP では常に空配列(JMnedict 統合後 = 2026 年 6 月以降に異読候補を populate)。
        姓 / 名タグが付かない token は surface fallback で reading 劣化、warning で明示。
      tags: [text]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NameReadingRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NameReadingResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        匿名の Free 枠(10,000 回/月)はキー無しで利用可。Starter / Pro / Enterprise
        プランは `shrb_` + 32 文字の英数字キーを発行。

  schemas:
    TokenizeRequest:
      type: object
      required: [text]
      properties:
        text:
          type: string
          description: 形態素解析する日本語テキスト
          example: 東京都港区六本木で打ち合わせをしました

    Token:
      type: object
      required: [surface, byte_start, byte_end, position, word_id, is_unknown, details]
      properties:
        surface:
          type: string
          description: トークン表層形
        byte_start:
          type: integer
          description: 入力中のバイト開始位置
        byte_end:
          type: integer
          description: 入力中のバイト終了位置
        position:
          type: integer
          description: トークン位置(0-based)
        word_id:
          type: integer
          description: IPAdic 内 ID(未知語は -1)
        is_unknown:
          type: boolean
          description: 辞書未登録か
        details:
          type: array
          items:
            type: string
          description: |
            IPAdic 品詞 / 活用 / 読み詳細。順序固定:
            [品詞大, 中, 小, 詳細, 活用型, 活用形, 原形, 読み, 発音]

    TokenizeResponse:
      type: object
      required: [text, tokens, token_count, attribution]
      properties:
        text:
          type: string
        tokens:
          type: array
          items:
            $ref: '#/components/schemas/Token'
        token_count:
          type: integer
        timing:
          $ref: '#/components/schemas/Timing'
        attribution:
          $ref: '#/components/schemas/DictionaryAttribution'

    NormalizeOptions:
      type: object
      properties:
        width:
          type: string
          enum: [half, full, preserve]
          default: preserve
          description: ASCII 全角/半角の統一方向
        kana:
          type: string
          enum: [hiragana, katakana, preserve]
          default: preserve
          description: ひらがな/カタカナ変換方向
        spaces:
          type: string
          enum: [single, trim, preserve]
          default: preserve
          description: 空白正規化(`single` は連続→単一、`trim` は加えて前後除去)
        halfwidth_kana:
          type: string
          enum: [expand, preserve]
          default: preserve
          description: |
            半角カタカナ(U+FF61-U+FF9F)の正規化。
            `expand` で全角カタカナへ変換、濁点 `ﾞ` / 半濁点 `ﾟ` は前文字と合成
            (例: `ｶﾞ` → `ガ`、`ﾊﾟ` → `パ`、`ｳﾞ` → `ヴ`)。句読点 `｡｢｣､･` も
            全角化。`kana: hiragana` と併用すると半角カナ → 全角カナ → ひらがな
            の連結変換が可能。
        sudachi:
          type: string
          enum: [apply, preserve]
          default: preserve
          description: |
            Sudachi 表記正規化(送り違い / 異体字 / カタカナ表記揺れ)。
            `apply` で Lindera tokenize + SudachiDict 由来 lookup map を適用、
            各トークンを normalized_form に置換。Phase 1+2 後に適用される
            (例: `行なう` → `行う`、`コンピュータ` → `コンピューター`、
            `デヴィルズ` → `デビルズ`)。Lindera 起動を伴うため cold start で
            +50-200 ms 程度。本番 5/18 リリース時に活性化、それまでは preserve のみ実効。

    NormalizeRequest:
      type: object
      required: [text]
      properties:
        text:
          type: string
          example: ＡＢＣ１２３ あいうえお アイウエオ
        options:
          $ref: '#/components/schemas/NormalizeOptions'

    NormalizeChange:
      type: object
      required: [type, before, after]
      properties:
        type:
          type: string
          enum: [width, kana, spaces, halfwidth_kana, sudachi]
        before:
          type: string
        after:
          type: string

    NormalizeResponse:
      type: object
      required: [text, normalized, changes, attribution]
      properties:
        text:
          type: string
        normalized:
          type: string
        changes:
          type: array
          items:
            $ref: '#/components/schemas/NormalizeChange'
          description: 適用された変換ステップ(noop は省略)
        timing:
          $ref: '#/components/schemas/NormalizeTiming'
          description: Phase 3 (sudachi="apply") を使った場合のみ含まれる
        attribution:
          oneOf:
            - $ref: '#/components/schemas/ServiceAttribution'
            - $ref: '#/components/schemas/SudachiAttribution'
          description: |
            Phase 1+2 のみ使用時は ServiceAttribution、Phase 3 (sudachi="apply") 適用時は
            SudachiAttribution(SudachiDict + Lindera + IPAdic の license / source 同梱)。

    FuriganaOptions:
      type: object
      properties:
        kana:
          type: string
          enum: [hiragana, katakana]
          default: hiragana

    FuriganaRequest:
      type: object
      required: [text]
      properties:
        text:
          type: string
          example: 東京都港区六本木で打ち合わせをしました
        options:
          $ref: '#/components/schemas/FuriganaOptions'

    FuriganaToken:
      type: object
      required: [surface, reading]
      properties:
        surface:
          type: string
        reading:
          type: string

    FuriganaResponse:
      type: object
      required: [text, tokens, attribution]
      properties:
        text:
          type: string
        tokens:
          type: array
          items:
            $ref: '#/components/schemas/FuriganaToken'
        timing:
          $ref: '#/components/schemas/Timing'
        attribution:
          $ref: '#/components/schemas/DictionaryAttribution'

    NameMatchedBy:
      type: string
      enum:
        - dictionary_both
        - dictionary_family_only
        - dictionary_given_only
        - whitespace
        - heuristic
        - empty
      description: |
        姓名分割 / 読み推定の判定経路。
        - `dictionary_both`: 隣接 姓+名 タグペア(信頼度最高、confidence 0.97)
        - `dictionary_family_only`: 姓 タグのみ + 後続を given 扱い(0.7)
        - `dictionary_given_only`: 名 タグのみ + 先行を family 扱い(0.7)
        - `whitespace`: 空白区切り 2 セグメント(0.6)
        - `heuristic`: 長さベース 1+1 / 2+1 / 2+残 split(0.4 + warning)
        - `empty`: 空入力(0)

    NameSplitRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: 分割対象の日本語人名
          example: 吉川良介

    NameSplitResponse:
      type: object
      required: [name, family, given, confidence, matched_by, attribution]
      properties:
        name:
          type: string
        family:
          type: string
          description: 姓(分割不能時は空文字)
          example: 吉川
        given:
          type: string
          description: 名(分割不能時は空文字)
          example: 良介
        confidence:
          type: number
          format: float
          description: 0-1 の信頼度。0.5 未満で warning 同梱
          example: 0.97
        warning:
          type: string
          enum: [low_confidence, empty_input]
          description: confidence < 0.5 または空入力時のみ含まれる
        matched_by:
          $ref: '#/components/schemas/NameMatchedBy'
        timing:
          $ref: '#/components/schemas/Timing'
        attribution:
          $ref: '#/components/schemas/DictionaryAttribution'

    NameReadingOptions:
      type: object
      properties:
        kana:
          type: string
          enum: [hiragana, katakana]
          default: hiragana
          description: 読みのカナ種別

    NameReadingRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: 読み推定対象の日本語人名
          example: 吉川良介
        options:
          $ref: '#/components/schemas/NameReadingOptions'

    NameReadingResponse:
      type: object
      required:
        - name
        - family
        - given
        - family_reading
        - given_reading
        - reading
        - candidates
        - confidence
        - matched_by
        - attribution
      properties:
        name:
          type: string
        family:
          type: string
          example: 吉川
        given:
          type: string
          example: 良介
        family_reading:
          type: string
          description: 姓の読み(辞書 hit 時のみ正確、surface fallback 時は surface 等)
          example: よしかわ
        given_reading:
          type: string
          example: りょうすけ
        reading:
          type: string
          description: family_reading + given_reading の連結
          example: よしかわりょうすけ
        candidates:
          type: array
          items:
            type: string
          description: |
            異読候補。**IPAdic only MVP では常に空配列**。
            2026 年 6 月のモノレポ化時に JMnedict user dictionary 統合で populate 予定。
        confidence:
          type: number
          format: float
          example: 0.97
        warning:
          type: string
          enum: [low_confidence, empty_input]
        matched_by:
          $ref: '#/components/schemas/NameMatchedBy'
        timing:
          $ref: '#/components/schemas/Timing'
        attribution:
          $ref: '#/components/schemas/DictionaryAttribution'

    Timing:
      type: object
      properties:
        tokenize_ms:
          type: integer
        setup_ms:
          type: integer
        cold_start:
          type: boolean
        r2_fetch_ms:
          type: integer
        tokenizer_init_ms:
          type: integer
        dict_total_bytes:
          type: integer

    DictionaryAttribution:
      type: object
      required: [dictionary, license, source]
      properties:
        dictionary:
          type: string
          example: IPAdic v3.0.7
        license:
          type: string
          example: BSD 3-Clause
        source:
          type: string
          format: uri
          example: https://github.com/lindera/lindera

    ServiceAttribution:
      type: object
      required: [service, url]
      properties:
        service:
          type: string
          example: shirabe-text-api
        url:
          type: string
          format: uri
          example: https://shirabe.dev

    SudachiAttribution:
      type: object
      required:
        - service
        - url
        - dictionary
        - dictionary_license
        - dictionary_source
        - tokenizer
        - tokenizer_license
      properties:
        service:
          type: string
          example: shirabe-text-api
        url:
          type: string
          format: uri
          example: https://shirabe.dev
        dictionary:
          type: string
          description: SudachiDict 由来の正規化マップ
          example: SudachiDict-small
        dictionary_license:
          type: string
          example: Apache-2.0
        dictionary_source:
          type: string
          format: uri
          example: https://github.com/WorksApplications/SudachiDict
        tokenizer:
          type: string
          example: Lindera + IPAdic v3.0.7
        tokenizer_license:
          type: string
          example: MIT (Lindera) / BSD 3-Clause (IPAdic)

    NormalizeTiming:
      type: object
      description: Phase 3 (sudachi="apply") 適用時のみ含まれる
      properties:
        setup_ms:
          type: integer
        cold_start:
          type: boolean
        tokenize_ms:
          type: integer
        sudachi_lookup_ms:
          type: integer
        map_fetch_ms:
          type: integer
          description: cold start 時のみ非ゼロ
        map_parse_ms:
          type: integer
          description: cold start 時のみ非ゼロ
        map_entries:
          type: integer
          description: SudachiDict 由来 lookup map のエントリ数

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object

  responses:
    BadRequest:
      description: 入力不正
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: 認証失敗
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: |
        レート制限 / 月間使用量超過。`error.code` は `RATE_LIMIT_EXCEEDED` または
        `USAGE_LIMIT_EXCEEDED`。レスポンスは AI agent が 1 hop で paid 切替できるよう
        `current_plan` / `next_plan` / `upgrade_url` / `pricing_url` を含む。
      headers:
        Retry-After:
          schema:
            type: integer
          description: 秒次レート制限の待機秒数
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
