跳转到主要内容

目的

直接利用 VOICEVOX 编辑器的 .vvproj,让 Laravel 能重新生成 Talk / Song。

顶层结构

.vvproj 是 UTF-8 JSON。Talk 与 Song 保存在同一个文件中。
{
  "appVersion": "0.25.2",
  "talk": {
    "audioKeys": [],
    "audioItems": {}
  },
  "song": {
    "tpqn": 480,
    "tempos": [],
    "timeSignatures": [],
    "tracks": {},
    "trackOrder": []
  }
}
Key内容
appVersion保存时的编辑器版本
talkTalk 用数据(audioKeys + audioItems
songSong 用数据(速度、拍号、轨道群)

talk 部分

talk.audioKeys 是有序数组,talk.audioItems 是以 ID 为键的 Record。
{
  "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

Key内容
text输入文本
voice.engineId引擎 ID(对应 /engine_manifest
voice.speakerId说话人 UUID
voice.styleIdStyle ID。可直接传给 /synthesis?speaker={styleId}
query等同于 AudioQuery
presetKey编辑器的预设 ID。Laravel 侧的预设请参见 预设
如果 .vvproj 已保存有 query,则可以不再执行 /audio_query,直接进入合成。

accentPhrases

Key内容
morasmora 数组(consonant 类字段可能被省略)
accent重音位置(从 1 开始)
pauseMora在标点等处插入静音时可能会存在
isInterrogative疑问句标志

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"]
}
Key内容
tpqnTicks Per Quarter Note。标准为 480
tempos速度表(position 为 tick)
timeSignatures拍号表(measureNumber 从 1 开始)
tracks以 Track ID 为键的 Record
trackOrder显示/播放顺序。需与 tracks 的键集合一致

Track

Key内容
singer.styleId最终传给 /frame_synthesis?speaker={styleId} 的 ID
notes音符数组
keyRangeAdjustment以半音为单位的调号调整
volumeRangeAdjustment音量调整
pitchEditData / volumeEditData按帧的编辑数据
phonemeTimingEditData每个 Note ID 的音素时长编辑
solo / mute用于判定播放对象轨道

Note

Key内容
id音符 ID(唯一)
position音符开始 tick
duration音符长度 tick
noteNumberMIDI 音符编号
lyric歌词

tick、秒、帧的转换

在单一速度下按以下方式换算。
seconds = ticks / tpqn * 60 / bpm
frames = round(seconds * frameRate)
在存在速度变化时,将 temposposition 升序累加各区间。
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;
}
使用 Note::len() helper 将 tick 转换为帧长的实现请参考 Score 与 Note 详解

Song 音频生成流程

直接编辑时的注意事项

  • 必须使 tracks 的键集合与 trackOrder 一致
  • talk.audioKeystalk.audioItems 同样需要保持同步
  • 保持 position >= 0duration >= 1noteNumber 处于 0..127
  • tempos[0].position 通常为 0timeSignatures[0].measureNumber 通常为 1
  • 未知的 key 尽可能保留并重新保存,以避免破坏与未来版本的兼容性

Laravel 代码示例

以下是读取 .vvproj 并生成 Talk 与 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:直接使用已保存的 query 进行合成
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:这里将第一条轨道的 duration 转为 frame_length 后生成
$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");
最后修改于 2026年7月13日