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

# Guide de développement d'applications intégrées à l'API du moteur - VOICEVOX for Laravel

> Pour développer une application cliente qui appelle l'API du moteur VOICEVOX : enregistrement d'URL, pipeline de synthèse chantée, cache et conception multi-moteur, tout du point de vue de l'implémentation.

# Guide de développement d'applications intégrées à l'API du moteur VOICEVOX

<Frame caption="Application 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>

Notes d'implémentation pour créer une application cliente qui appelle l'API du moteur VOICEVOX.
Sur la base des retours d'implémentation d'une application Song pour macOS, ce document organise les points à considérer pour utiliser l'API du moteur de manière stable depuis une application externe, plutôt que de porter tel quel l'architecture interne de l'éditeur officiel VOICEVOX.

Pour rester utilisable même en dehors de Xcode/Swift, les principes généraux sont présentés en premier, avec des compléments Xcode à la fin de chaque section.

<Info>
  Cette page reprend le [guide d'origine](https://github.com/invokable/laravel-voicevox/blob/main/docs/develop/voicevox-engine-api-app-guide.md) et le documente sans abréger le contenu.
</Info>

## 1. Enregistrer d'abord le moteur par son URL

<Frame caption="Configuration de la connexion au moteur">
  <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>

L'éditeur officiel VOICEVOX démarre lui-même l'exécutable du moteur (fourni ou spécifié) et gère les paramètres du moteur et le fichier `engine_manifest.json`. Dans une application cliente externe, il est en revanche plus pratique d'adopter un modèle où l'on enregistre d'abord « l'URL d'un serveur HTTP API déjà démarré ».

Voici les URL typiques.

| Moteur                   | Exemple                  |
| ------------------------ | ------------------------ |
| Moteur VOICEVOX officiel | `http://127.0.0.1:50021` |
| Moteur version Laravel   | `http://127.0.0.1:50513` |

Le moteur officiel dispose d'un ajustement automatique du port : si 50021 est utilisé, il démarre sur 50022 ou un port libre suivant.
La version Laravel démarrée sans `--port=50513` (donc sur le port 8000 par défaut) essaie aussi 8001 et suivants, mais si l'on spécifie un port, l'ajustement automatique est désactivé.

Lors de l'enregistrement, ne stockez pas seulement la chaîne d'URL : effectuez au minimum une vérification de connexion via les API suivantes et enregistrez les métadonnées récupérées avec l'URL.

| API                                                     | Usage                                                                                            |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `GET /engine_manifest`                                  | Récupère `uuid`, le nom du moteur, `frame_rate`, la fréquence d'échantillonnage par défaut, etc. |
| `GET /version`                                          | Récupère la version pour l'affichage et la vérification de compatibilité                         |
| `GET /singers`                                          | Récupère la liste des styles de chant                                                            |
| `GET /singer_info?speaker_uuid=...&resource_format=...` | Récupère l'icône et les informations complémentaires                                             |

Point important : ne stockez pas l'URL dans les projets ou les pistes, mais `engineId` et `styleId`. L'URL change d'un environnement utilisateur à l'autre, alors que `engineId` correspond au `uuid` de `/engine_manifest` et s'aligne aussi facilement avec `singer.engineId` de `.vvproj`.

**Complément Xcode :** dans l'application Song, `RegisteredEngine` stocke `baseURL`, `engineId`, `name`, `frameRate`, `defaultSamplingRate`, `version` et `singers` dans `UserDefaults`. Normalisez l'URL (slash final, query, fragment) avant utilisation pour traiter `http://127.0.0.1:50021/` et `http://127.0.0.1:50021` comme identiques. Sur une application macOS utilisant des connexions HTTP locales, vérifiez aussi les réglages sandbox et App Transport Security.

## 2. Ne pas prendre en charge le démarrage du moteur comme le fait l'application officielle

Vouloir implémenter dès le départ, dans une application externe, la même gestion de démarrage du moteur que l'éditeur officiel impose de gérer le démarrage de processus par OS, les binaires embarqués, les conflits de ports, les mises à jour et les logs. En particulier lorsque l'on utilise une autre implémentation que le moteur officiel, l'approche « lancer un exécutable de moteur à un emplacement donné » colle mal.

Il est plus sûr de fixer d'abord ce fonctionnement :

1. L'utilisateur démarre lui-même le moteur au préalable.
2. Il saisit l'URL dans l'application.
3. L'application récupère les métadonnées et enregistre le moteur.
4. Au moment du rendu, l'`engineId` enregistré est résolu en URL.

Cette approche permet de traiter le moteur officiel, la version Laravel, un moteur dans Docker ou sur un autre hôte sous la même abstraction. En cas d'échec de connexion, indiquez clairement l'action suivante à l'utilisateur, par exemple : « Démarrez le moteur puis reconnectez-vous ».

**Complément Xcode :** en SwiftUI, isolez un écran « Connexion au moteur » avec un `TextField` pour l'URL, des boutons `Enregistrer / Reconnecter`, des boutons de préréglage officiel/Laravel et la liste des moteurs enregistrés. Pendant une connexion asynchrone, affichez un `ProgressView` ; pour les erreurs de connexion, plutôt que d'afficher tel quel le message de `LocalizedError`, préfixez une explication à destination de l'utilisateur.

## 3. Laisser choisir les chanteurs et styles après enregistrement du moteur

Dans l'API VOICEVOX, la voix effectivement utilisée pour la génération est déterminée par le style ID passé au paramètre de requête `speaker`. En Song, chaque piste possède son style de chant : l'interface développe donc `singers` depuis le moteur enregistré et propose comme options le nom du chanteur, le nom du style et le style ID.

Il suffit de stocker les deux valeurs suivantes :

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

Cette structure s'aligne bien avec les données Song compatibles `.vvproj` et facilite aussi la prise en charge multi-moteur. Si à l'ouverture d'un projet l'`engineId` correspondant n'est pas enregistré, affichez-le comme « moteur non enregistré » et invitez à enregistrer l'URL.

**Complément Xcode :** en Swift, faites porter à la piste un `Singer(engineId: String, styleId: Int)` ; pour l'affichage, aplatissez `RegisteredEngine.singers` dans un tableau ViewModel du type `SingerStyleOption`. Quand l'assignation d'un chanteur change, invalidez les audios déjà rendus.

## 4. Pour la compatibilité `.vvproj`, adopter un modèle Song basé sur les ticks

Les données Song de l'éditeur VOICEVOX représentent les notes et le tempo en ticks. L'API du moteur a in fine besoin d'une longueur en frames, mais garder le modèle d'édition interne en ticks convient mieux pour le piano roll, les changements de tempo, la signature rythmique, l'Undo/Redo et la lecture/écriture `.vvproj`.

La structure minimale est la suivante.

| Élément       | Champs principaux                                          |
| ------------- | ---------------------------------------------------------- |
| 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`                       |

Ce n'est qu'au moment du rendu que l'on convertit les ticks en secondes via la table de tempo et que l'on calcule `frame_length` selon le `frame_rate` du moteur.

**Complément Xcode :** pour lire/écrire du JSON de type `.vvproj` avec `Codable`, séparez `tracks` (Dictionary indexé par ID) et `trackOrder` (tableau d'ordre d'affichage). Avant l'enregistrement, validez les doublons de `trackOrder`, les ID inexistants et les pistes non réordonnées pour éviter les défauts d'affichage après lecture.

## 5. Implémenter la synthèse Song comme une API en 4 étapes

Le rendu Song implique plus d'étapes que le `/audio_query` → `/synthesis` de Talk. Le pipeline de base comprend 4 étapes.

```mermaid theme={null}
sequenceDiagram
  participant App as Application<br>cliente
  participant Engine as Moteur<br>VOICEVOX

  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
```

Pour `/sing_frame_audio_query`, `/sing_frame_f0` et `/sing_frame_volume`, on utilise l'ID de style du singing teacher, `6000` ; seul le dernier `/frame_synthesis` utilise le `styleId` du chanteur réellement sélectionné. Cette valeur peut être traitée sous la même hypothèse pour le moteur officiel comme pour la version Laravel.

Dans les requêtes de ces API, il est essentiel de préserver la correspondance entre `Score` et `FrameAudioQuery`. `Score.notes[].frame_length` doit être calculé à partir du `frame_rate` du moteur, et les silences se représentent par `key: null`, `lyric: ""`.

**Complément Xcode :** en Swift, définissez un protocol `VoicevoxEngineSongAPI` et implémentez-le via un client `URLSession` : c'est plus simple à tester. Pour les endpoints retournant du JSON, mettez `Content-Type` / `Accept` à `application/json` ; la réponse de `/frame_synthesis` est un WAV binaire à traiter comme `Data`. Tout code HTTP différent de 2xx doit produire une erreur incluant le code de statut et le corps de la réponse.

## 6. Découper par phrase et mettre en cache

Synthétiser l'intégralité de la Song en une seule passe à chaque édition régénère toutes les pistes même pour une petite modification, ce qui dégrade la réactivité de l'interface. Comme dans l'éditeur officiel, il est plus commode de regrouper les notes contiguës en phrases séparées par des silences.

Mettez en cache par phrase les étapes suivantes.

| Cache      | À inclure dans l'entrée                                     |
| ---------- | ----------------------------------------------------------- |
| AudioQuery | engineId, frame rate, tempo, Score, ajustement de tessiture |
| F0         | AudioQuery, éditions de hauteur, position de la phrase      |
| Volume     | AudioQuery, F0, valeurs affectant la génération du volume   |
| Voice/WAV  | Query de synthèse, style ID final                           |

Dans l'implémentation initiale, il est acceptable d'invalider en bloc l'audio rendu d'une piste dès qu'une édition la touche. Concevez la clé de cache à partir d'un hash de l'entrée pour pouvoir ajouter plus tard une invalidation différentielle par phrase.

**Complément Xcode :** un `actor` portant le cache de rendu facilite la gestion d'état avec Swift Concurrency. Incrémentez un numéro de génération à chaque nouveau rendu et n'utilisez pas les réponses d'API en retard. Combinez `Task.checkCancellation()` et le contrôle de génération pour gérer à la fois l'annulation et le re-rendu.

## 7. Déterminer frame rate et silences depuis les métadonnées du moteur

`ScoreNote.frame_length` se calcule à partir des secondes et du `frame_rate`. N'inscrivez pas `frame_rate` en valeur constante : récupérez-le lors de l'enregistrement via `/engine_manifest` et stockez-le.

Lors de la synthèse Song, insérez des silences en tête et en fin de phrase. Si le silence de tête est trop court, la génération des phonèmes peut devenir instable ; équilibrez donc en implémentation entre « silence réel », « noire » et « tick calculé à partir d'un nombre minimum de secondes ». Ajoutez aussi un court silence en fin ; au besoin, appliquez un fade-out sur ce dernier silence.

Corrigez également la longueur en frames pour qu'après arrondi elle ne devienne pas nulle ou négative. Sur des notes très courtes ou juste après un changement de tempo, l'erreur d'arrondi produit facilement des longueurs de 0 frame.

**Complément Xcode :** en faisant de `tickToSecond` / `secondToTick` des fonctions pures testées, vous utilisez la même conversion pour le piano roll, la tête de lecture, le rendu et l'export vidéo. Pour `frameLength = Int(round(seconds * frameRate))`, garantissez au minimum 1 frame en reportant l'écart sur la note voisine.

## 8. Distinguer, pour l'édition des paramètres, « avant génération » et « avant synthèse »

En Song, l'ajustement de tessiture, du volume, de la hauteur, du volume par frame et de la temporisation des phonèmes agissent chacun à un stade différent.

| Édition                                  | Moment d'application                                                     |
| ---------------------------------------- | ------------------------------------------------------------------------ |
| `keyRangeAdjustment`                     | Clé lors de la génération du Score, shift de hauteur après génération F0 |
| `volumeRangeAdjustment`                  | Tableau de volume avant `/frame_synthesis`                               |
| Édition de hauteur                       | Tableau F0 après `/sing_frame_f0`                                        |
| Édition de volume                        | Tableau volume après `/sing_frame_volume`                                |
| Édition de la temporisation des phonèmes | `FrameAudioQuery.phonemes`                                               |

Décidez tôt quelle édition invalide quel cache : cela évite de casser le rendu lorsqu'on ajoute plus tard des fonctions d'édition d'interface.

**Complément Xcode :** ne dispersez pas l'application des éditions de paramètres dans les ViewModels ; regroupez-la dans une logique pure comme `SongParameterEditApplicator`, plus facile à tester. Corrigez ou transformez en erreur de manière explicite les accès hors-plage aux tableaux et les volumes négatifs.

## 9. Utiliser la même logique de piste pour lecture, mixage et export

Après le rendu, on place les WAV par phrase sur la timeline pour la lecture. En multi-piste, si l'évaluation de solo/mute/gain/pan diverge entre lecture normale, export global du WAV et export par stem, le comportement devient imprévisible pour l'utilisateur.

Règle commune : s'il existe au moins une piste solo, ne sortir que les pistes solo ; sinon, sortir les pistes non muettes. Pour l'export stem, sortez uniquement la piste ciblée, indépendamment de l'état solo/mute habituel.

**Complément Xcode :** sur macOS, on peut construire le graphe de lecture avec `AVAudioEngine`, `AVAudioPlayerNode` et `AVAudioMixerNode`. Pour l'export, prévoyez un chemin dédié au rendu hors ligne, distinct de la lecture, et mutualisez l'encodage WAV. Si une édition invalide l'audio rendu pendant la lecture, arrêtez la lecture et signalez l'état.

## 10. L'interface doit distinguer « non enregistré », « non assigné » et « non rendu »

Dans une application intégrée à l'API du moteur VOICEVOX, les causes d'échec se répartissent en plusieurs cas.

| État                                | Message à afficher                                      |
| ----------------------------------- | ------------------------------------------------------- |
| Moteur non enregistré               | Enregistrer l'URL depuis l'écran de connexion au moteur |
| Moteur non démarré                  | Démarrer le moteur puis se reconnecter                  |
| `engineId` du projet non enregistré | Enregistrer l'URL du moteur correspondant               |
| Piste sans chanteur assigné         | Choisir un style de chanteur enregistré                 |
| Audio obsolète après édition        | Un re-rendu est nécessaire                              |
| Rendu en cours                      | Rendre l'annulation possible                            |

Regrouper tout cela sous « lecture impossible » masque la cause réelle. Séparez les états et indiquez la prochaine action dans l'interface.

**Complément Xcode :** dans le ViewModel SwiftUI, un `renderingState` sous forme d'enum (`idle`, `rendering`, `rendered`, `stale`, `failed`) rend cohérents la désactivation des boutons et l'affichage du statut. Prévoyez d'afficher les messages d'erreur non seulement dans la barre de statut mais aussi via des alertes.

## 11. En multi-moteur, séparer les durées de vie de « URL » et de « engineId »

En multi-moteur, la confusion la plus fréquente vient du rôle différent de l'URL et de l'`engineId`.

| Valeur     | Durée de vie                                     | Utilisation              |
| ---------- | ------------------------------------------------ | ------------------------ |
| URL        | Change selon l'environnement utilisateur         | Cible des appels API     |
| `engineId` | Identifiant côté implémentation/modèle du moteur | Référence dans le projet |
| `styleId`  | Identifiant de style dans le moteur              | Paramètre `speaker`      |

Ne stockez pas l'URL dans le fichier de projet : stockez `engineId` et `styleId`. La correspondance `engineId -> URL` est portée par la configuration de l'application. Ainsi, même un projet reçu de quelqu'un d'autre peut être rendu sur votre poste dès que vous enregistrez le même moteur par son URL.

**Complément Xcode :** avant le rendu, résolvez `engineId` en `RegisteredEngine` avec par exemple `Dictionary(uniqueKeysWithValues: registeredEngines.map { ($0.engineID, $0) })`. Si aucun ne correspond, échouez à l'avance avec « le moteur du chanteur n'est pas enregistré », sans envoyer de requête HTTP.

## 12. Séparer les tests entre client HTTP, conversions de modèle et plan de rendu

Faire tourner en permanence le moteur VOICEVOX pour les tests est lourd : dans les tests automatisés courants, on mocke HTTP et l'on vérifie la génération des requêtes et les transitions d'état côté application.

Voici les éléments à tester en priorité.

| Cible                    | À vérifier                                                                                                   |
| ------------------------ | ------------------------------------------------------------------------------------------------------------ |
| Normalisation d'URL      | Les slashes finaux et URL avec chemin sont traités comme prévu                                               |
| Enregistrement de moteur | `/engine_manifest`, `/version`, `/singers`, `/singer_info` sont appelés et le modèle de stockage est produit |
| E/S `.vvproj`            | Cohérence entre `trackOrder` et `tracks`, gestion des versions anciennes/futures                             |
| Conversion tick/seconde  | Les conversions traversant des changements de tempo sont réversibles                                         |
| Génération de Score      | Silences, `frame_length`, paroles par défaut, ajustement de tessiture                                        |
| Plan de rendu            | Détecter moteur non enregistré, chanteur non défini, `engineId` incohérent avant l'appel API                 |
| Cache                    | Réutilisation pour la même entrée, invalidation après édition                                                |

**Complément Xcode :** en substituant un `URLProtocol` dans un `URLSessionConfiguration.ephemeral`, vous testez un client `URLSession` sans serveur HTTP réel. Injectez l'API dans le renderer via un protocol, et exposez une méthode retournant un snapshot de l'état de cache d'un `actor` pour faciliter les vérifications.

## Ordre d'implémentation recommandé

Pour partir d'une configuration minimale, cet ordre convient bien.

<Steps>
  <Step title="Enregistrement de l'URL du moteur et sauvegarde des métadonnées">
    Enregistrez l'URL et stockez avec elle les métadonnées récupérées via `/engine_manifest` ou `/version`.
  </Step>

  <Step title="Liste des styles de chanteur et assignation">
    Affichez la liste des styles de chanteur et assignez `engineId + styleId` aux pistes.
  </Step>

  <Step title="Modèle basé sur les ticks">
    Fixez d'abord le modèle Song/Track/Note en ticks.
  </Step>

  <Step title="Client d'API en 4 étapes">
    Implémentez la génération de `Score` et le client de l'API Song en 4 étapes.
  </Step>

  <Step title="Rendu par phrase">
    Introduisez le rendu par phrase et le cache.
  </Step>

  <Step title="Fonction de lecture">
    Rendez les WAV rendus lisibles sur la timeline.
  </Step>

  <Step title="Évaluation multi-piste">
    Unifiez l'évaluation solo/mute/gain/pan multi-piste.
  </Step>

  <Step title="Export">
    Ajoutez l'export WAV et l'export par stem.
  </Step>

  <Step title="Fonctions d'édition">
    Ajoutez l'édition de hauteur, de volume et de temporisation des phonèmes.
  </Step>

  <Step title="Fonctions avancées">
    Passez ensuite aux fonctions avancées comme la compatibilité `.vvproj` en lecture/écriture ou l'export vidéo.
  </Step>
</Steps>

Plutôt que de viser d'emblée l'ensemble des fonctionnalités de l'éditeur officiel, fixez d'abord l'enregistrement d'URL, l'assignation de chanteur, l'API en 4 étapes et la gestion de l'état de re-rendu : cela s'aligne aussi bien avec le moteur officiel qu'avec la version Laravel.

## Liens associés

* [Page du package VOICEVOX Core for PHP](/fr/packages/voicevox-core-php)
* [Mode API du moteur : Talk](/fr/packages/laravel-voicevox/engine-talk)
* [Mode API du moteur : Song](/fr/packages/laravel-voicevox/engine-song)
* [Explications détaillées sur Score et Note](/fr/packages/laravel-voicevox/song-score-note)
* [Spécification du fichier .vvproj](/fr/packages/laravel-voicevox/vvproj)


## Related topics

- [Mode API du moteur : Talk (synthèse vocale de texte) - VOICEVOX for Laravel](/fr/packages/laravel-voicevox/engine-talk.md)
- [Mode API du moteur : Song (synthèse vocale chantée) - VOICEVOX for Laravel](/fr/packages/laravel-voicevox/engine-song.md)
- [VOICEVOX for Laravel](/fr/packages/laravel-voicevox/index.md)
- [Mode client - Presets - VOICEVOX for Laravel](/fr/packages/laravel-voicevox/client-presets.md)
- [Prise en main - VOICEVOX for Laravel](/fr/packages/laravel-voicevox/getting-started.md)
