Guida allo sviluppo di app che integrano l’API del motore VOICEVOX

Questa pagina documenta senza omissioni i contenuti della guida originale.
1. Registra prima il motore tramite URL

engine_manifest.json. In un’app client esterna, invece, è più semplice adottare un approccio in cui si registra “l’URL del server HTTP API già avviato”.
Gli URL rappresentativi sono i seguenti.
| Motore | Esempio |
|---|---|
| Motore VOICEVOX ufficiale | http://127.0.0.1:50021 |
| Versione Laravel del motore | http://127.0.0.1:50513 |
--port=50513 sulla porta predefinita 8000, si avvia sulla 8001 in caso di conflitto, ma se specifichi la porta la funzione di regolazione è disabilitata.
Al momento della registrazione, invece di salvare solo la stringa URL, chiama come conferma di connessione almeno le seguenti API e salva l’URL insieme alle meta-informazioni ottenute.
| API | Scopo |
|---|---|
GET /engine_manifest | Ottiene uuid, nome del motore, frame_rate, sampling rate predefinita, ecc. |
GET /version | Ottiene la versione per visualizzazione e controllo compatibilità |
GET /singers | Ottiene l’elenco degli stili di canto |
GET /singer_info?speaker_uuid=...&resource_format=... | Ottiene icone e informazioni aggiuntive |
engineId e styleId. L’URL cambia in base all’ambiente dell’utente, mentre engineId corrisponde al campo uuid di /engine_manifest e si allinea facilmente anche a singer.engineId di .vvproj.
Note per Xcode: nell’app per canzoni, RegisteredEngine contiene baseURL, engineId, name, frameRate, defaultSamplingRate, version, singers e viene salvato in UserDefaults. Se normalizzi l’URL rimuovendo slash finale, query e fragment prima dell’uso, puoi trattare http://127.0.0.1:50021/ e http://127.0.0.1:50021 come identici. Se usi connessioni HTTP locali in un’app macOS, verifica anche le impostazioni di sandbox e App Transport Security.
2. Non gestire nell’app “l’avvio del motore” come l’app ufficiale
Se in un’app esterna cerchi di implementare fin dall’inizio la stessa gestione dell’avvio del motore dell’editor ufficiale, dovrai occuparti di avvio dei processi per ogni OS, binari inclusi, conflitti di porta, aggiornamenti e gestione dei log. In particolare, quando si utilizzano implementazioni del motore diverse da quella ufficiale, l’approccio di specificare la posizione dell’eseguibile del motore per avviarlo è di difficile integrazione. Inizialmente è sicuro attenersi al seguente flusso operativo.- L’utente avvia il motore per primo.
- Nell’app si inserisce l’URL.
- L’app ottiene le meta-informazioni e le registra.
- Al momento del rendering, l’
engineIdregistrato viene risolto in URL.
TextField per l’URL, i pulsanti Registra / Riconnetti, i pulsanti preset per la versione ufficiale/Laravel e l’elenco dei motori registrati. Durante la connessione asincrona mostra un ProgressView e per gli errori di connessione è meglio anteporre una spiegazione rivolta all’utente al posto del semplice testo di LocalizedError.
3. Fai scegliere il cantante e lo stile dopo la registrazione del motore
Nelle API di VOICEVOX, la voce utilizzata infine per generare l’audio è determinata dallo style ID passato al parametro queryspeaker. Nelle canzoni, ogni traccia ha uno stile di cantante; nella UI, espandi singers dai motori registrati e proponi nome del cantante, nome dello stile e style ID come opzioni selezionabili.
I valori da salvare sono sufficienti nei due seguenti.
.vvproj e supporti bene il multi-motore. Quando apri un progetto, se il relativo engineId non è registrato, mostralo come “motore non registrato” e invita a registrare l’URL.
Note per Xcode: in Swift, mantieni sulla traccia Singer(engineId: String, styleId: Int) e appiattisci per la visualizzazione i dati da RegisteredEngine.singers in un array di ViewModel come SingerStyleOption. Quando cambi l’assegnazione del cantante, invalida l’audio già renderizzato.
4. Se pensi alla compatibilità con .vvproj, usa un modello di canzone basato su tick
I dati canzone dell’editor VOICEVOX hanno note e tempi basati sui tick. L’API del motore richiede infine una lunghezza in frame, ma è preferibile mantenere il modello di editing interno all’app basato sui tick, per una migliore integrazione con piano roll, cambio di tempo, tempo in chiave, Undo/Redo e lettura/scrittura di .vvproj.
La struttura minima è la seguente.
| Elemento | Campi principali |
|---|---|
| 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 in base al frame_rate del motore.
Note per Xcode: quando leggi/scrivi JSON in stile .vvproj con Codable, separa tracks come Dictionary con ID come chiavi e trackOrder come array dell’ordine di visualizzazione. Prima del salvataggio, verifica duplicati di trackOrder, ID inesistenti e tracce non ordinate per prevenire disallineamenti UI al caricamento.
5. Implementa la sintesi delle canzoni come API in 4 fasi
Il rendering delle canzoni ha più passaggi rispetto al/audio_query → /synthesis del talk. La pipeline base è composta da 4 fasi.
In /sing_frame_audio_query, /sing_frame_f0 e /sing_frame_volume usa lo style ID 6000 del singing teacher, mentre solo nell’ultima chiamata a /frame_synthesis usa lo styleId del cantante effettivamente selezionato. Questo valore può essere gestito con le stesse premesse sia sul motore ufficiale che sulla versione Laravel.
Nelle richieste di ciascuna API è importante non rompere la corrispondenza tra Score e FrameAudioQuery. Score.notes[].frame_length si calcola dal frame_rate del motore, e il silenzio si rappresenta con key: null, lyric: "".
Note per Xcode: in Swift crea un protocol come VoicevoxEngineSongAPI e implementa il client con URLSession per facilitare i test. Negli endpoint che restituiscono JSON, imposta Content-Type / Accept a application/json; la risposta di /frame_synthesis va trattata come Data binario WAV. Trasforma le risposte HTTP diverse da 2xx in errori che includono status code e corpo della risposta.
6. Suddividi in frasi e usa la cache
Se sintetizzi ogni volta l’intera canzone in un’unica passata, anche una piccola modifica comporta la rigenerazione di tutte le tracce, peggiorando la reattività della UI. Come nell’editor ufficiale, è pratico un design che suddivide gruppi di note consecutive in frasi separate dalle pause. Metti in cache le seguenti fasi per ogni frase.| Cache | Elementi in input |
|---|---|
| AudioQuery | engineId, frame rate, tempo, Score, regolazione della tessitura |
| F0 | AudioQuery, modifiche di pitch, posizione della frase |
| Volume | AudioQuery, F0, valori che influenzano la generazione del volume |
| Voice/WAV | Query per la sintesi, style ID finale |
actor per gestire lo stato con Swift Concurrency. Incrementa il numero di generazione all’avvio di un nuovo rendering e ignora le risposte API tardive. Combinando Task.checkCancellation() e la verifica di generazione puoi gestire sia le cancellazioni che i re-render.
7. Determina frame rate e pause dalle meta-informazioni del motore
ScoreNote.frame_length si calcola dai secondi e dal frame_rate. Non fissare frame_rate come valore hard-coded: ottienilo da /engine_manifest in fase di registrazione e salvalo.
Nella sintesi delle canzoni, inserisci pause all’inizio e alla fine della frase. Se la pausa iniziale è troppo breve, la generazione dei fonemi può diventare instabile: nell’implementazione trova un equilibrio tra “la pausa effettiva”, “una nota da un quarto” e “un tick calcolato a ritroso da un minimo di secondi”. Aggiungi anche un breve silenzio alla fine e, se necessario, applica un fade out all’ultima sezione di silenzio.
Inoltre, correggi la lunghezza in frame in modo che non risulti ≤ 0 a causa degli arrotondamenti. Nelle note brevi o subito dopo un cambio di tempo, gli errori di arrotondamento producono facilmente 0 frame.
Note per Xcode: se rendi tickToSecond / secondToTick funzioni pure e le testi, puoi utilizzare la stessa conversione per piano roll, playhead, rendering ed esportazione video. Assicurati che frameLength = Int(round(seconds * frameRate)) garantisca almeno 1 frame, spostando eventuali differenze sulle note adiacenti.
8. Distingui dove agiscono le modifiche dei parametri: “prima della generazione” o “prima della sintesi”
Nelle canzoni, le modifiche di tessitura, volume, pitch, volume ed epigrafia dei fonemi influiscono ciascuna su fasi diverse.| Modifica | Fase di applicazione |
|---|---|
keyRangeAdjustment | Chiave alla generazione dello Score, pitch shift dopo la generazione di F0 |
volumeRangeAdjustment | Array di volume prima di /frame_synthesis |
| Modifica di pitch | Array F0 dopo /sing_frame_f0 |
| Modifica di volume | Array volume dopo /sing_frame_volume |
| Modifica dei tempi dei fonemi | FrameAudioQuery.phonemes |
SongParameterEditApplicator per facilitare i test. Corregge esplicitamente o trasforma in errore gli accessi fuori range agli array e i volumi negativi.
9. Riproduzione, mix ed esportazione usano lo stesso giudizio sulle tracce
Dopo il rendering, posiziona sulla timeline i WAV per ogni frase e riproducili. Nel supporto multi-traccia, se la valutazione di solo/mute/gain/pan differisce tra riproduzione normale, esportazione WAV completa ed esportazione per stem, il comportamento diventa imprevedibile per l’utente. Come regola comune, se esiste almeno una traccia solo, riproduci solo le tracce solo; se non ci sono tracce solo, riproduci le tracce non mute. Nell’esportazione per stem, esporta la traccia bersaglio in isolamento, separandola dallo stato solo/mute normale. Note per Xcode: su macOS puoi costruire il grafo di riproduzione conAVAudioEngine, AVAudioPlayerNode e AVAudioMixerNode. Predisponi un percorso di rendering offline separato dalla riproduzione normale per l’esportazione, e centralizza la codifica WAV. Se durante la riproduzione avviene una modifica e l’audio renderizzato viene invalidato, ferma la riproduzione e comunica chiaramente lo stato.
10. La UI distingue “non registrato”, “non assegnato” e “non renderizzato”
Nelle app che integrano l’API del motore VOICEVOX, i motivi di fallimento si dividono in più casi.| Stato | Messaggio da mostrare |
|---|---|
| Motore non registrato | Registra l’URL nella schermata di connessione |
| Motore non avviato | Avvia il motore e riconnetti |
engineId del progetto non registrato | Aggiungi la registrazione dell’URL del motore corrispondente |
| Cantante non assegnato alla traccia | Seleziona uno stile di cantante registrato |
| Audio obsoleto dopo la modifica | È necessario il re-render |
| Rendering in corso | Rendi possibile annullare |
renderingState come un enum con valori tipo idle, rendering, rendered, stale, failed per allineare il controllo di disabled dei pulsanti alla visualizzazione dello stato. Consenti di mostrare i messaggi di errore non solo nella status bar ma anche in un alert.
11. Nel multi-motore separa i cicli di vita di “URL” e “engineId”
Nel supporto multi-motore, ciò che genera confusione è la differenza di ruolo tra URL eengineId.
| Valore | Ciclo di vita | Utilizzo |
|---|---|---|
| URL | Cambia in base all’ambiente dell’utente | Destinazione delle chiamate API |
engineId | Identificatore lato implementazione del motore/modelli | Riferimento all’interno del progetto |
styleId | Identificatore di stile all’interno del motore | Parametro speaker |
engineId e styleId. Nelle impostazioni dell’app mantieni la corrispondenza engineId -> URL. In questo modo, anche un progetto ricevuto da qualcun altro può essere renderizzato registrando nel tuo ambiente lo stesso motore tramite URL.
Note per Xcode: prima del rendering, risolvi da engineId a RegisteredEngine con qualcosa come Dictionary(uniqueKeysWithValues: registeredEngines.map { ($0.engineID, $0) }). Se non trovi corrispondenza, non inviare la richiesta HTTP e fallisci in anticipo con “il motore del cantante non è registrato”.
12. Suddividi i test tra HTTP client, conversione del modello e piano di rendering
Poiché è oneroso mantenere sempre acceso il motore VOICEVOX per i test, nei test automatici standard è preferibile mockare l’HTTP e verificare la generazione delle richieste e le transizioni di stato lato app. Ecco gli elementi da testare in via prioritaria.| Obiettivo | Cosa verificare |
|---|---|
| Normalizzazione URL | Slash finale e URL con path sono gestiti come atteso |
| Registrazione del motore | Chiami /engine_manifest, /version, /singers, /singer_info e costruisci il modello da salvare |
I/O di .vvproj | Coerenza tra trackOrder e tracks, gestione di versioni vecchie/future |
| Conversione tick/secondi | Le conversioni che attraversano cambi di tempo sono roundtrip |
| Generazione dello Score | Pause, frame_length, testo predefinito, regolazione della tessitura |
| Piano di rendering | Motore non registrato, cantante non impostato, engineId non corrispondente vengono rilevati prima delle chiamate API |
| Cache | Riuso con lo stesso input e invalidazione dopo la modifica |
URLSessionConfiguration.ephemeral con URLProtocol sostituito, puoi testare il client URLSession senza un server HTTP reale. Inietta l’API nel renderer tramite protocol e per lo stato della cache in un actor fornisci metodi che restituiscano snapshot per facilitare la verifica.
Ordine consigliato di implementazione
Se parti da una configurazione minima, questo è un buon ordine.Registrazione dell'URL del motore e salvataggio delle meta-informazioni
Registra l’URL e salva insieme le meta-informazioni ottenute da
/engine_manifest e /version.Elenco degli stili di cantante e assegnazione
Mostra l’elenco degli stili di cantante e assegna
engineId + styleId alle tracce.Client dell'API in 4 fasi
Implementa la generazione dello
Score e il client dell’API in 4 fasi per le canzoni.