Vai al contenuto principale

Obiettivo

Sfruttare direttamente i file .vvproj dell’editor VOICEVOX per rigenerare talk e song in Laravel.

Struttura di primo livello

.vvproj è JSON in UTF-8. Talk e song sono salvati nello stesso file.
{
  "appVersion": "0.25.2",
  "talk": {
    "audioKeys": [],
    "audioItems": {}
  },
  "song": {
    "tpqn": 480,
    "tempos": [],
    "timeSignatures": [],
    "tracks": {},
    "trackOrder": []
  }
}
ChiaveContenuto
appVersionVersione dell’editor al momento del salvataggio
talkDati per il talk (audioKeys + audioItems)
songDati per il song (tempo, tempo in chiave, tracce)

Sezione talk

talk.audioKeys è un array ordinato, talk.audioItems è un Record con l’ID come chiave.
{
  "audioKeys": ["audio-item-uuid"],
  "audioItems": {
    "audio-item-uuid": {
      "text": "ずんだもんなのだ",
      "voice": {
        "engineId": "engine-uuid",
        "speakerId": "speaker-uuid",
        "styleId": 3
      },
      "query": {
        "accentPhrases": [],
        "speedScale": 1,
        "pitchScale": 0
      },
      "presetKey": "preset-uuid"
    }
  }
}

TalkAudioItem

ChiaveContenuto
textTesto di input
voice.engineIdID engine (corrisponde a /engine_manifest)
voice.speakerIdUUID del parlante
voice.styleIdID stile. Da passare così com’è a /synthesis?speaker={styleId}
queryEquivalente ad AudioQuery
presetKeyID preset dell’editor. Per i preset lato Laravel consulta Preset
Se .vvproj contiene già query, puoi passare direttamente alla sintesi senza rieseguire /audio_query.

accentPhrases

ChiaveContenuto
morasArray di mora (le consonant possono essere omesse)
accentPosizione dell’accento (partendo da 1)
pauseMoraPuò comparire quando si inseriscono silenzi in corrispondenza di punteggiatura ecc.
isInterrogativeFlag di interrogativa

Sezione song

{
  "tpqn": 480,
  "tempos": [{ "position": 0, "bpm": 120 }],
  "timeSignatures": [{ "measureNumber": 1, "beats": 4, "beatType": 4 }],
  "tracks": {
    "track-uuid": {
      "name": "無名トラック",
      "singer": { "engineId": "engine-uuid", "styleId": 3003 },
      "notes": []
    }
  },
  "trackOrder": ["track-uuid"]
}
ChiaveContenuto
tpqnTicks Per Quarter Note. Il valore standard è 480
temposMappa dei tempi (position in tick)
timeSignaturesMappa dei tempi in chiave (measureNumber parte da 1)
tracksRecord con l’ID della traccia come chiave
trackOrderOrdine di visualizzazione/riproduzione. Deve coincidere con l’insieme delle chiavi di tracks

Track

ChiaveContenuto
singer.styleIdID da passare infine a /frame_synthesis?speaker={styleId}
notesArray di note
keyRangeAdjustmentAggiustamento della tonalità in semitoni
volumeRangeAdjustmentAggiustamento del volume
pitchEditData / volumeEditDataDati di modifica a livello di frame
phonemeTimingEditDataModifiche di timing dei fonemi per ciascun Note ID
solo / muteUtilizzati per determinare le tracce oggetto di riproduzione

Note

ChiaveContenuto
idID della nota (univoco)
positionTick di inizio nota
durationDurata della nota in tick
noteNumberNumero di nota MIDI
lyricTesto

Conversione tra tick, secondi e frame

Con tempo unico si converte così:
seconds = ticks / tpqn * 60 / bpm
frames = round(seconds * frameRate)
Con cambi di tempo, somma i segmenti scorrendo tempos in ordine crescente di position.
function ticksToSeconds(int $targetTick, int $tpqn, array $tempos): float
{
    $seconds = 0.0;
    $currentTick = 0;

    foreach ($tempos as $index => $tempo) {
        $nextTick = $tempos[$index + 1]['position'] ?? $targetTick;
        $segmentEnd = min($targetTick, $nextTick);

        if ($segmentEnd <= $currentTick) {
            break;
        }

        $bpm = $tempo['bpm'];
        $seconds += (($segmentEnd - $currentTick) / $tpqn) * (60 / $bpm);
        $currentTick = $segmentEnd;
    }

    return $seconds;
}
Per l’implementazione della conversione da tick a lunghezza di frame con l’helper Note::len(), consulta Dettagli di Score e Note.

Flusso di generazione audio per il song

Attenzioni nella modifica diretta

  • L’insieme delle chiavi di tracks deve sempre coincidere con trackOrder
  • Analogamente sincronizza talk.audioKeys e talk.audioItems
  • Mantieni position >= 0, duration >= 1 e noteNumber in 0..127
  • Di solito tempos[0].position è 0 e timeSignatures[0].measureNumber è 1
  • Se possibile, conserva le chiavi sconosciute nel risalvataggio per non rompere la compatibilità con versioni future

Esempio in codice Laravel

Esempio minimo per caricare un .vvproj e generare talk e song.
use Illuminate\Support\Facades\Storage;
use Revolution\Voicevox\Client\TalkAudioQuery;
use Revolution\Voicevox\Song\Note;
use Revolution\Voicevox\Song\Score;
use Revolution\Voicevox\Voicevox;

$project = json_decode(
    Storage::disk('local')->get('voicevox/sample.vvproj'),
    true,
    flags: JSON_THROW_ON_ERROR,
);

// Talk: sintetizza usando direttamente la query salvata
foreach ($project['talk']['audioKeys'] as $audioKey) {
    $item = $project['talk']['audioItems'][$audioKey];

    Voicevox::talk($item['text'], id: $item['voice']['styleId'])
        ->tap(fn (TalkAudioQuery $talk) => $talk->audioQuery = array_replace($talk->audioQuery, $item['query']))
        ->generate(id: $item['voice']['styleId'])
        ->storeAs('vvproj/talk', "{$audioKey}.wav");
}

// Song: qui convertiamo la duration della prima traccia in frame_length e generiamo
$trackId = $project['song']['trackOrder'][0];
$track = $project['song']['tracks'][$trackId];
$bpm = $project['song']['tempos'][0]['bpm'] ?? 120;

$score = Score::make([
    Note::make(length: 15, lyric: '', key: null),
    ...collect($track['notes'])->map(
        fn (array $note) => Note::make(
            length: Note::len(ticks: $note['duration'], bpm: $bpm),
            lyric: $note['lyric'] ?? 'ら',
            key: $note['noteNumber'],
            id: $note['id'] ?? null,
        ),
    )->all(),
    Note::make(length: 2, lyric: '', key: null),
]);

Voicevox::song($score, teacher: 6000)
    ->generate(id: $track['singer']['styleId'])
    ->storeAs('vvproj/song', "{$trackId}.wav");
Ultima modifica il 13 luglio 2026