Passer au contenu principal

Objectif

Utiliser tel quel le .vvproj de l’éditeur VOICEVOX pour regénérer Talk et Song depuis Laravel.

Structure de haut niveau

.vvproj est un JSON UTF-8. Il conserve Talk et Song dans un même fichier.
{
  "appVersion": "0.25.2",
  "talk": {
    "audioKeys": [],
    "audioItems": {}
  },
  "song": {
    "tpqn": 480,
    "tempos": [],
    "timeSignatures": [],
    "tracks": {},
    "trackOrder": []
  }
}
CléContenu
appVersionVersion de l’éditeur au moment de la sauvegarde
talkDonnées Talk (audioKeys + audioItems)
songDonnées Song (tempo, mesure, ensemble de pistes)

Section talk

talk.audioKeys est un tableau ordonné ; talk.audioItems est un Record indexé par ID.
{
  "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

CléContenu
textTexte saisi
voice.engineIdID du moteur (correspond à /engine_manifest)
voice.speakerIdUUID du locuteur
voice.styleIdID de style. Passé tel quel à /synthesis?speaker={styleId}
queryÉquivalent à un AudioQuery
presetKeyID de preset dans l’éditeur. Pour les presets côté Laravel, consultez Presets
Si query est déjà stocké dans le .vvproj, on peut passer directement à la synthèse sans réexécuter /audio_query.

accentPhrases

CléContenu
morasTableau de mores (les éléments comme consonant peuvent être omis)
accentPosition de l’accent (indexée à partir de 1)
pauseMoraPeut apparaître pour insérer un silence après une ponctuation
isInterrogativeDrapeau d’interrogation

Section 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"]
}
CléContenu
tpqnTicks Per Quarter Note. Standard : 480
temposTable des tempos (position en ticks)
timeSignaturesTable des signatures rythmiques (measureNumber commence à 1)
tracksRecord indexé par ID de piste
trackOrderOrdre d’affichage et de lecture. Doit correspondre à l’ensemble des clés de tracks

Track

CléContenu
singer.styleIdID finalement passé à /frame_synthesis?speaker={styleId}
notesTableau de notes
keyRangeAdjustmentAjustement de tessiture en demi-tons
volumeRangeAdjustmentAjustement de volume
pitchEditData / volumeEditDataDonnées d’édition par frame
phonemeTimingEditDataÉdition de la temporisation des phonèmes par ID de note
solo / muteUtilisés pour déterminer les pistes à lire

Note

CléContenu
idID de la note (unique)
positionTick de début
durationLongueur en ticks
noteNumberNuméro de note MIDI
lyricParoles

Conversion tick / seconde / frame

Avec un tempo unique, la conversion est la suivante.
seconds = ticks / tpqn * 60 / bpm
frames = round(seconds * frameRate)
Avec plusieurs tempos, cumulez les segments en parcourant tempos par position croissant.
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;
}
Pour l’implémentation de la conversion de ticks vers une longueur en frames via le helper Note::len(), consultez Détails Score et Note.

Flux de génération de la voix Song

Précautions pour l’édition directe

  • Faites toujours correspondre l’ensemble des clés de tracks et trackOrder
  • Synchronisez de la même manière talk.audioKeys et talk.audioItems
  • Respectez position >= 0, duration >= 1 et noteNumber dans 0..127
  • Habituellement tempos[0].position vaut 0 et timeSignatures[0].measureNumber vaut 1
  • Conservez si possible les clés inconnues à la réécriture pour ne pas casser la compatibilité avec les versions futures

Exemple de code Laravel

Exemple minimal pour charger un .vvproj et générer Talk et 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 : synthèse directe à partir de la query stockée
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 : ici on convertit la `duration` de la première piste en frame_length et on génère
$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");
Dernière modification le 13 juillet 2026