> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 引擎 API 整合應用開發指南 - VOICEVOX for Laravel

> 為了開發呼叫 VOICEVOX 引擎 API 的 client 應用,從 URL 註冊、song 合成 pipeline、快取到多引擎設計,以實作觀點整理。

# VOICEVOX 引擎 API 整合應用開發指南

<Frame caption="Song 應用">
  <img src="https://mintcdn.com/invokable/BODEyCSwaMBfQXfp/images/laravel-voicevox/song-app-1.png?fit=max&auto=format&n=BODEyCSwaMBfQXfp&q=85&s=91a6f894c9a5c128517f125e1afaa5ce" alt="VOICEVOX Song" width="3106" height="1646" data-path="images/laravel-voicevox/song-app-1.png" />
</Frame>

開發呼叫 VOICEVOX 引擎 API 的 client 應用時的實作筆記。\
以 macOS 用 song 應用的實作紀錄為基礎,不將官方 VOICEVOX 編輯器的內部結構原樣移植,而是整理作為外部應用穩定使用引擎 API 所需的項目。

為了讓 Xcode/Swift 以外的環境也能使用,先寫下通用方針,並在各項目結尾附上 Xcode 的補充說明。

<Info>
  本頁基於 [原文指南](https://github.com/invokable/laravel-voicevox/blob/main/docs/develop/voicevox-engine-api-app-guide.md) 文件化,內容並未省略。
</Info>

## 1. 一開始就以 URL 註冊引擎

<Frame caption="引擎連線設定">
  <img src="https://mintcdn.com/invokable/-Erq4G4RzGqd8RU2/images/laravel-voicevox/song-app-2.png?fit=max&auto=format&n=-Erq4G4RzGqd8RU2&q=85&s=f8676c5ab7b58f87d87c2585da87ff58" alt="VOICEVOX Song" width="2024" height="1248" data-path="images/laravel-voicevox/song-app-2.png" />
</Frame>

官方 VOICEVOX 編輯器會由應用啟動內建或指定的引擎執行檔,並以引擎設定或 `engine_manifest.json` 為前提管理。相較之下,外部 client 應用先「註冊已啟動的 HTTP API 伺服器 URL」的方式更好處理。

代表性的 URL 如下。

| 引擎             | 範例                       |
| -------------- | ------------------------ |
| 官方 VOICEVOX 引擎 | `http://127.0.0.1:50021` |
| Laravel 版引擎    | `http://127.0.0.1:50513` |

官方引擎有自動 port 調整功能,若 50021 已被使用會以 50022 之後空著的 port 啟動。\
Laravel 版若未指定 `--port=50513` 而以預設的 8000 啟動,會以 8001 之後啟動,但指定 port 時調整功能會被停用。

註冊時不要僅保存 URL 字串,而是作為連線確認至少呼叫下列 API,並將取得的 metadata 一併保存。

| API                                                     | 用途                                             |
| ------------------------------------------------------- | ---------------------------------------------- |
| `GET /engine_manifest`                                  | 取得 `uuid`、引擎名稱、`frame_rate`、預設 sampling rate 等 |
| `GET /version`                                          | 取得用於顯示與相容性確認的版本                                |
| `GET /singers`                                          | 取得歌唱 style 清單                                  |
| `GET /singer_info?speaker_uuid=...&resource_format=...` | 取得圖示與額外資訊                                      |

重點是,專案或 track 不要保存 URL,而是 `engineId` 與 `styleId`。URL 因使用者環境不同而異,但 `engineId` 對應 `/engine_manifest` 的 `uuid`,也容易對應到 `.vvproj` 的 `singer.engineId`。

**Xcode 補充:** Song 應用中,在 `RegisteredEngine` 保存 `baseURL`、`engineId`、`name`、`frameRate`、`defaultSamplingRate`、`version`、`singers`,並存入 `UserDefaults`。URL 先將結尾斜線、query、fragment 正規化後再使用,可將 `http://127.0.0.1:50021/` 與 `http://127.0.0.1:50021` 視為相同。macOS 應用使用 HTTP 本地連線時,也請確認 sandbox 與 App Transport Security 的設定。

## 2. 不要像官方應用一樣抱著「引擎啟動」

若要以外部應用一開始就實作與官方編輯器相同的引擎啟動管理,需要處理 OS 別的程序啟動、內建 binary、port 衝突、更新、log 管理。特別是使用非官方引擎的實作時,以指定引擎執行檔位置來啟動的方式較不契合。

首先固定為以下運作方式較為安全。

1. 使用者先啟動引擎。
2. 於應用輸入 URL。
3. 應用取得 metadata 並註冊。
4. 渲染時從已註冊的 `engineId` 解析為 URL。

此方式下,官方引擎、Laravel 版引擎、Docker 上的引擎、其他 host 的引擎都能以相同的抽象處理。無法連線時明確告知使用者「請啟動引擎後再連線」等下一步操作。

**Xcode 補充:** SwiftUI 中將「引擎連線」畫面獨立,放上 `TextField` 讓輸入 URL、`註冊 / 重新連線` 按鈕、官方/Laravel 版預設按鈕與已註冊引擎清單,運作起來會較順利。非同步連線中顯示 `ProgressView`,連線錯誤時建議先加上對使用者的說明,而非直接顯示 `LocalizedError` 的訊息。

## 3. 歌手/Style 由使用者於註冊引擎後選擇

在 VOICEVOX 的 API 中,最終音訊產生使用的聲音由傳給 `speaker` query 參數的 style ID 決定。Song 中每個 track 都持有歌手 style,因此 UI 上會展開已註冊引擎的 `singers`,並將歌手名、style 名、style ID 作為選項。

保存的值有以下 2 個即可。

```jsonc theme={null}
{
  "singer": {
    "engineId": "engine-uuid",
    "styleId": 6000
  }
}
```

此結構容易對齊 `.vvproj` 相容的 song 資料,也易於支援多引擎。開啟專案時若對應的 `engineId` 未註冊,顯示為「未註冊引擎」並提示註冊 URL。

**Xcode 補充:** Swift 中讓 track 持有 `Singer(engineId: String, styleId: Int)`,顯示時可從 `RegisteredEngine.singers` 平坦化為 `SingerStyleOption` 等 ViewModel 用陣列。更改歌手指派時,已渲染的音訊應失效。

## 4. 考慮 `.vvproj` 相容時採用以 tick 為基準的 song 模型

VOICEVOX 編輯器的 song 資料以 tick 為基準保存音符與 tempo。引擎 API 本身最終需要 frame length,但應用內的編輯模型採用 tick 基準對於 piano roll、tempo 變更、拍號、Undo/Redo、`.vvproj` 讀寫較為友善。

最基本的結構如下。

| 元素            | 主要欄位                                                   |
| ------------- | ------------------------------------------------------ |
| Song          | `tpqn`、`tempos`、`timeSignatures`、`tracks`、`trackOrder` |
| Track         | `name`、`singer`、`notes`、`gain`、`pan`、`solo`、`mute`     |
| Note          | `id`、`position`、`duration`、`noteNumber`、`lyric`        |
| Tempo         | `position`、`bpm`                                       |
| TimeSignature | `measureNumber`、`beats`、`beatType`                     |

只在渲染時使用 tempo map 將 tick 轉為秒,並依引擎的 `frame_rate` 計算 `frame_length`。

**Xcode 補充:** 用 `Codable` 讀寫 `.vvproj` 風格 JSON 時,`tracks` 使用以 ID 為 key 的 Dictionary,`trackOrder` 分開為顯示順序陣列。保存前檢查 `trackOrder` 重複、不存在的 ID、未排序的 track,可避免載入後 UI 崩壞。

## 5. Song 合成以 4 階段 API 實作

Song 的渲染比 Talk 的 `/audio_query` → `/synthesis` 多出許多步驟。基本 pipeline 分為以下 4 階段。

```mermaid theme={null}
sequenceDiagram
  participant App as Client<br>應用
  participant Engine as VOICEVOX<br>引擎

  App->>Engine: POST /sing_frame_audio_query?speaker=6000
  Engine-->>App: FrameAudioQuery
  App->>Engine: POST /sing_frame_f0?speaker=6000
  Engine-->>App: f0[]
  App->>Engine: POST /sing_frame_volume?speaker=6000
  Engine-->>App: volume[]
  App->>Engine: POST /frame_synthesis?speaker=track.styleId
  Engine-->>App: WAV
```

`/sing_frame_audio_query`、`/sing_frame_f0`、`/sing_frame_volume` 使用 singing teacher 的 style ID `6000`,只有最後的 `/frame_synthesis` 才使用實際選取的歌手 `styleId`。此值在官方引擎與 Laravel 版引擎都以相同前提處理。

各 API 請求中,重要的是不破壞 `Score` 與 `FrameAudioQuery` 的對應。`Score.notes[].frame_length` 從引擎的 `frame_rate` 計算,靜音以 `key: null`、`lyric: ""` 表示。

**Xcode 補充:** Swift 中建立 `VoicevoxEngineSongAPI` 之類的 protocol,實作以 `URLSession` client 進行,便於測試。回傳 JSON 的 endpoint 中將 `Content-Type` / `Accept` 設為 `application/json`,而 `/frame_synthesis` 的回應以 WAV binary 的 `Data` 處理。HTTP 非 2xx 時應包含狀態碼與回應內容的錯誤。

## 6. 以片段(phrase)為單位分割並快取

若每次都將整首歌一次合成,只要小幅編輯就會導致全 track 重新產生,UI 反應變差。與官方編輯器類似,將連續音符群分為 phrase 並以休止符分割的設計較易處理。

每個 phrase 快取以下各階段。

| 快取         | 輸入包含項目                            |
| ---------- | --------------------------------- |
| AudioQuery | 引擎 ID、frame rate、tempo、Score、音域調整 |
| F0         | AudioQuery、pitch 編輯、phrase 位置     |
| Volume     | AudioQuery、F0、影響音量產生的值            |
| Voice/WAV  | 合成用 Query、最終 style ID             |

初期實作中,當有編輯時可直接使該 track 的渲染音訊全部失效。設計 cache key 時以輸入 hash 建立,方便日後加入以 phrase 為單位的差異失效。

**Xcode 補充:** 讓 `actor` 持有渲染快取,便於在 Swift Concurrency 上進行狀態管理。新渲染開始時世代編號往前推進,即便舊 API 回應返回也不採用。搭配 `Task.checkCancellation()` 與世代檢查,可同時處理取消與重新渲染。

## 7. 從引擎 metadata 決定 frame rate 與休止

`ScoreNote.frame_length` 由秒數與 `frame_rate` 計算。不要將 `frame_rate` 寫死為固定值,而是註冊時從 `/engine_manifest` 取得後保存。

Song 合成中,phrase 開頭與結尾要插入休止符。開頭休止太短時音素生成可能不穩定,實作時要在「實際的休止」「四分音符」「從最低秒數反推的 tick」之間取得平衡。結尾也加入短的無音,必要時對最後無音區間 fade out。

此外,frame length 若因四捨五入變成 0 以下要進行修正。短音符或 tempo 變更後,容易因四捨五入誤差產生 0 frame。

**Xcode 補充:** 將 `tickToSecond` / `secondToTick` 設計為純函式並測試後,可於 piano roll、播放頭、渲染、影片輸出中使用相同的轉換。`frameLength = Int(round(seconds * frameRate))` 的結果建議將差值移至相鄰音符等方式,以保證最低 1 frame。

## 8. 參數編輯依「產生前」與「合成前」分開

Song 中,音域、音量、pitch、volume、音素 timing 編輯分別影響不同階段。

| 編輯                      | 反映時機                               |
| ----------------------- | ---------------------------------- |
| `keyRangeAdjustment`    | Score 產生時的 key、F0 產生後的 pitch shift |
| `volumeRangeAdjustment` | `/frame_synthesis` 前的音量陣列          |
| Pitch 編輯                | `/sing_frame_f0` 後的 F0 陣列          |
| Volume 編輯               | `/sing_frame_volume` 後的 volume 陣列  |
| 音素 timing 編輯            | `FrameAudioQuery.phonemes`         |

儘早決定哪個編輯會使哪些快取失效,日後追加 UI 編輯功能時,渲染結果就不易崩壞。

**Xcode 補充:** 參數編輯的套用不要散落在 ViewModel,建議集中在 `SongParameterEditApplicator` 之類的純邏輯中,便於測試。陣列越界存取與負值音量應明確修正或錯誤化。

## 9. 播放、mix、輸出使用相同的 track 判定

渲染後將 phrase 的 WAV 排放在 timeline 上播放。多 track 支援時,若一般播放、整體 WAV 輸出、stem 輸出中 solo/mute/gain/pan 判定不同,對使用者而言行為會難以預期。

作為共用規則:只要有 1 個 solo track 就只輸出 solo track;沒有 solo 時輸出未 mute 的 track。Stem 輸出時單獨輸出目標 track,與一般 solo/mute 狀態分開考量。

**Xcode 補充:** macOS 上可用 `AVAudioEngine`、`AVAudioPlayerNode`、`AVAudioMixerNode` 建立播放 graph。輸出時另外準備一條 offline 渲染路徑,將 WAV 編碼共用。播放中若編輯導致已渲染音訊失效,應停止播放並明確標示狀態。

## 10. UI 區分「未註冊、未指派、未渲染」

在 VOICEVOX 引擎 API 整合應用中,失敗原因可分為以下數種。

| 狀態                 | 應顯示的說明         |
| ------------------ | -------------- |
| 引擎未註冊              | 在引擎連線畫面註冊 URL  |
| 引擎未啟動              | 啟動引擎並重新連線      |
| 專案的 `engineId` 未註冊 | 追加註冊對應的引擎 URL  |
| Track 未指派歌手        | 選擇已註冊的歌手 style |
| 編輯後音訊過舊            | 需要重新渲染         |
| 渲染中                | 提供可取消          |

若都合併為「無法播放」則使用者無法得知原因。應分開狀態,並在 UI 顯示下一步操作。

**Xcode 補充:** SwiftUI 的 ViewModel 中將 `renderingState` 定為 `idle`、`rendering`、`rendered`、`stale`、`failed` 之類的 enum,可讓按鈕的 disabled 控制與狀態顯示一致。錯誤訊息除了狀態列,也應可顯示在 alert。

## 11. 多引擎中將「URL」與「engineId」的存續期分開

多引擎支援時容易混淆的是 URL 與 `engineId` 的角色不同。

| 值          | 存續期            | 用法              |
| ---------- | -------------- | --------------- |
| URL        | 隨使用者環境變化       | 作為 API 呼叫目標     |
| `engineId` | 引擎實作/模型端的識別碼   | 作為專案內參照         |
| `styleId`  | 引擎內的 style 識別碼 | 作為 `speaker` 參數 |

專案檔案中不要嵌入 URL,而是保存 `engineId` 與 `styleId`。應用設定端持有 `engineId -> URL` 的對應。這樣即使從他人接收專案,只要在自己的環境中以 URL 註冊相同引擎即可渲染。

**Xcode 補充:** 用 `Dictionary(uniqueKeysWithValues: registeredEngines.map { ($0.engineID, $0) })` 於渲染前從 `engineId` 解析為 `RegisteredEngine`。找不到時不應發送 HTTP 請求,而是事先以「歌手的引擎未註冊」失敗。

## 12. 測試分開 HTTP client、模型轉換、渲染計畫

由於一直啟動 VOICEVOX 引擎本體測試較重,一般自動測試中對 HTTP 進行 mock,確認應用端的請求產生與狀態轉移。

優先測試的項目如下。

| 對象             | 需確認事項                                                               |
| -------------- | ------------------------------------------------------------------- |
| URL 正規化        | 結尾斜線或帶路徑 URL 是否如預期處理                                                |
| 引擎註冊           | 呼叫 `/engine_manifest`、`/version`、`/singers`、`/singer_info`,能建立保存用模型 |
| `.vvproj` I/O  | trackOrder 與 tracks 的一致性、舊/未來版本的處理                                  |
| tick/second 轉換 | 跨 tempo 變更的轉換可以往返                                                   |
| Score 生成       | 休止、frame\_length、預設歌詞、音域調整                                          |
| 渲染計畫           | 未註冊引擎、未設定歌手、engineId 不匹配應在 API 呼叫前偵測                                |
| 快取             | 相同輸入可重用,編輯後失效                                                       |

**Xcode 補充:** 使用替換 `URLProtocol` 的 `URLSessionConfiguration.ephemeral`,不需實際 HTTP 伺服器即可測試 `URLSession` client。渲染器透過 protocol 注入 API,`actor` 的快取狀態提供傳回快照的方法便於驗證。

## 建議實作順序

從最小構成開始的話,以下順序較佳。

<Steps>
  <Step title="引擎 URL 註冊與 metadata 保存">
    註冊 URL,並將從 `/engine_manifest` 或 `/version` 取得的 metadata 一併保存。
  </Step>

  <Step title="歌手 style 一覽與指派">
    顯示歌手 style 清單,將 `engineId + styleId` 指派給 track。
  </Step>

  <Step title="以 tick 為基準的模型">
    先固化以 tick 為基準的 Song/Track/Note 模型。
  </Step>

  <Step title="4 階段 API client">
    實作 `Score` 生成與 4 階段 song API client。
  </Step>

  <Step title="以 phrase 分割的渲染">
    引入以 phrase 為單位的渲染與快取。
  </Step>

  <Step title="播放功能">
    讓已渲染的 WAV 能在 timeline 上播放。
  </Step>

  <Step title="多 track 判定">
    統一多 track 的 solo/mute/gain/pan 判定。
  </Step>

  <Step title="輸出">
    加入 WAV 輸出與 stem 輸出。
  </Step>

  <Step title="編輯功能">
    加入 pitch、volume、音素 timing 編輯。
  </Step>

  <Step title="進階功能">
    進一步發展 `.vvproj` 相容讀寫或影片輸出等進階功能。
  </Step>
</Steps>

與其從一開始就追求官方編輯器的所有功能,不如先固化 URL 註冊、歌手指派、4 階段 API、重新渲染狀態,這樣不論對官方引擎或 Laravel 版引擎都容易對齊。

## 相關連結

* [VOICEVOX Core for PHP 套件頁](/zh-TW/packages/voicevox-core-php)
* [引擎 API 模式:Talk](/zh-TW/packages/laravel-voicevox/engine-talk)
* [引擎 API 模式:Song](/zh-TW/packages/laravel-voicevox/engine-song)
* [Score 與 Note 詳細說明](/zh-TW/packages/laravel-voicevox/song-score-note)
* [.vvproj 檔案規範](/zh-TW/packages/laravel-voicevox/vvproj)


## Related topics

- [VOICEVOX for Laravel](/zh-TW/packages/laravel-voicevox/index.md)
- [入門 - VOICEVOX for Laravel](/zh-TW/packages/laravel-voicevox/getting-started.md)
- [Laravel AI SDK 整合 - VOICEVOX for Laravel](/zh-TW/packages/laravel-voicevox/ai-sdk.md)
- [Laravel MCP](/zh-TW/mcp.md)
- [Engine API 模式:Talk(文字語音合成)- VOICEVOX for Laravel](/zh-TW/packages/laravel-voicevox/engine-talk.md)
