메인 콘텐츠로 건너뛰기

목적

VOICEVOX 에디터의 .vvproj를 그대로 활용해서 Laravel에서 토크·송을 재생성할 수 있게 합니다.

최상위 구조

.vvproj는 UTF-8 JSON입니다. 토크와 송을 같은 파일에 저장합니다.
{
  "appVersion": "0.25.2",
  "talk": {
    "audioKeys": [],
    "audioItems": {}
  },
  "song": {
    "tpqn": 480,
    "tempos": [],
    "timeSignatures": [],
    "tracks": {},
    "trackOrder": []
  }
}
내용
appVersion저장 시의 에디터 버전
talk토크용 데이터 (audioKeys + audioItems)
song송용 데이터 (템포·박자·트랙군)

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

내용
text입력 텍스트
voice.engineId엔진 ID (/engine_manifest와 대응)
voice.speakerId화자 UUID
voice.styleId스타일 ID. /synthesis?speaker={styleId}에 그대로 전달
queryAudioQuery 상당
presetKey에디터의 프리셋 ID. Laravel 측 프리셋은 프리셋을 참조
.vvprojquery가 저장되어 있으면 /audio_query를 재실행하지 않고 그대로 합성으로 진행할 수 있습니다.

accentPhrases

내용
moras모라 배열 (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"]
}
내용
tpqnTicks Per Quarter Note. 표준은 480
tempos템포 맵 (position은 tick)
timeSignatures박자 맵 (measureNumber는 1부터 시작)
tracksTrack ID를 키로 하는 Record
trackOrder표시·재생 순. tracks의 키 집합과 일치시킴

Track

내용
singer.styleId최종적으로 /frame_synthesis?speaker={styleId}에 전달할 ID
notes노트 배열
keyRangeAdjustment반음 단위의 키 조정
volumeRangeAdjustment볼륨 조정
pitchEditData / volumeEditData프레임 단위의 편집 데이터
phonemeTimingEditDataNote ID별 음소 타이밍 편집
solo / mute재생 대상 트랙 판정에 사용

Note

내용
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() 헬퍼로 tick에서 프레임 길이로 변환하는 구현은 Score와 Note 상세를 참조하세요.

송 음성 생성 흐름

직접 편집 시 주의점

  • tracks의 키 집합과 trackOrder를 반드시 일치시킵니다.
  • talk.audioKeystalk.audioItems도 마찬가지로 동기화합니다.
  • position >= 0, duration >= 1, noteNumber0..127을 유지합니다.
  • tempos[0].position은 통상 0, timeSignatures[0].measureNumber는 통상 1.
  • 알 수 없는 키는 가능하면 유지한 채 재저장하여, 미래 버전과의 호환성을 깨뜨리지 않습니다.

Laravel 코드 예

.vvproj를 읽어 들여 토크와 송을 생성하는 최소 예입니다.
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일