Passer au contenu principal

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

VOICEVOX Song
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.
Cette page reprend le guide d’origine et le documente sans abréger le contenu.

1. Enregistrer d’abord le moteur par son URL

VOICEVOX Song
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.
MoteurExemple
Moteur VOICEVOX officielhttp://127.0.0.1:50021
Moteur version Laravelhttp://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.
APIUsage
GET /engine_manifestRécupère uuid, le nom du moteur, frame_rate, la fréquence d’échantillonnage par défaut, etc.
GET /versionRécupère la version pour l’affichage et la vérification de compatibilité
GET /singersRé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 :
{
  "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émentChamps principaux
Songtpqn, tempos, timeSignatures, tracks, trackOrder
Trackname, singer, notes, gain, pan, solo, mute
Noteid, position, duration, noteNumber, lyric
Tempoposition, bpm
TimeSignaturemeasureNumber, 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. 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
AudioQueryengineId, frame rate, tempo, Score, ajustement de tessiture
F0AudioQuery, éditions de hauteur, position de la phrase
VolumeAudioQuery, F0, valeurs affectant la génération du volume
Voice/WAVQuery 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.
ÉditionMoment d’application
keyRangeAdjustmentClé lors de la génération du Score, shift de hauteur après génération F0
volumeRangeAdjustmentTableau de volume avant /frame_synthesis
Édition de hauteurTableau F0 après /sing_frame_f0
Édition de volumeTableau volume après /sing_frame_volume
Édition de la temporisation des phonèmesFrameAudioQuery.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.
ÉtatMessage à 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 éditionUn re-rendu est nécessaire
Rendu en coursRendre 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.
ValeurDurée de vieUtilisation
URLChange selon l’environnement utilisateurCible des appels API
engineIdIdentifiant côté implémentation/modèle du moteurRéférence dans le projet
styleIdIdentifiant de style dans le moteurParamè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’URLLes 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 .vvprojCohérence entre trackOrder et tracks, gestion des versions anciennes/futures
Conversion tick/secondeLes conversions traversant des changements de tempo sont réversibles
Génération de ScoreSilences, frame_length, paroles par défaut, ajustement de tessiture
Plan de renduDétecter moteur non enregistré, chanteur non défini, engineId incohérent avant l’appel API
CacheRé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.
1

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.
2

Liste des styles de chanteur et assignation

Affichez la liste des styles de chanteur et assignez engineId + styleId aux pistes.
3

Modèle basé sur les ticks

Fixez d’abord le modèle Song/Track/Note en ticks.
4

Client d'API en 4 étapes

Implémentez la génération de Score et le client de l’API Song en 4 étapes.
5

Rendu par phrase

Introduisez le rendu par phrase et le cache.
6

Fonction de lecture

Rendez les WAV rendus lisibles sur la timeline.
7

Évaluation multi-piste

Unifiez l’évaluation solo/mute/gain/pan multi-piste.
8

Export

Ajoutez l’export WAV et l’export par stem.
9

Fonctions d'édition

Ajoutez l’édition de hauteur, de volume et de temporisation des phonèmes.
10

Fonctions avancées

Passez ensuite aux fonctions avancées comme la compatibilité .vvproj en lecture/écriture ou l’export vidéo.
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

Dernière modification le 13 juillet 2026