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

Cette page reprend le guide d’origine et le documente sans abréger le contenu.
1. Enregistrer d’abord le moteur par son URL

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 |
--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 |
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 :- L’utilisateur démarre lui-même le moteur au préalable.
- Il saisit l’URL dans l’application.
- L’application récupère les métadonnées et enregistre le moteur.
- Au moment du rendu, l’
engineIdenregistré est résolu en URL.
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êtespeaker. 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 :
.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 |
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.
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 |
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 |
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 avecAVAudioEngine, 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 |
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 |
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 |
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.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.Liste des styles de chanteur et assignation
Affichez la liste des styles de chanteur et assignez
engineId + styleId aux pistes.