Skip to content

Commit

Permalink
synthesis_morphingのテスト
Browse files Browse the repository at this point in the history
  • Loading branch information
qryxip committed Jan 4, 2024
1 parent 03d5055 commit 9c41398
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 38 deletions.
1 change: 1 addition & 0 deletions crates/voicevox_core_c_api/tests/e2e/testcases/morph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ impl assert_cdylib::TestCase for TestCase {
CStr::from_ptr(morphable_targets).to_bytes(),
)?[&self.target_style];

// TODO: スナップショットテストをやる
let result = {
const MORPH_RATE: f64 = 0.5;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@

import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Stream;
import jp.hiroshiba.voicevoxcore.Synthesizer.MorphableTargetInfo;
import jp.hiroshiba.voicevoxcore.exceptions.InferenceFailedException;
import jp.hiroshiba.voicevoxcore.exceptions.InvalidModelDataException;
import jp.hiroshiba.voicevoxcore.exceptions.SpeakerFeatureException;
import jp.hiroshiba.voicevoxcore.exceptions.StyleNotFoundException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

class SynthesizerTest extends TestUtils {
@FunctionalInterface
Expand All @@ -29,44 +35,6 @@ void checkIsGpuMode() {
assertFalse(synthesizer.isGpuMode());
}

@Test
void checkMorphableTargets() throws InvalidModelDataException {
OpenJtalk openJtalk = loadOpenJtalk();
Synthesizer synthesizer =
Synthesizer.builder(openJtalk).accelerationMode(Synthesizer.AccelerationMode.CPU).build();

synthesizer.loadVoiceModel(loadModel());

Map<Integer, MorphableTargetInfo> morphableTargets = synthesizer.morphableTargets(0);
assertFalse(morphableTargets.get(0).isMorphable);
assertFalse(morphableTargets.get(1).isMorphable);
assertFalse(morphableTargets.get(302).isMorphable);
assertFalse(morphableTargets.get(303).isMorphable);

morphableTargets = synthesizer.morphableTargets(1);
assertFalse(morphableTargets.get(0).isMorphable);
assertTrue(morphableTargets.get(1).isMorphable);
assertFalse(morphableTargets.get(302).isMorphable);
assertFalse(morphableTargets.get(303).isMorphable);

morphableTargets = synthesizer.morphableTargets(302);
assertFalse(morphableTargets.get(0).isMorphable);
assertFalse(morphableTargets.get(1).isMorphable);
assertTrue(morphableTargets.get(302).isMorphable);
assertTrue(morphableTargets.get(303).isMorphable);

morphableTargets = synthesizer.morphableTargets(303);
assertFalse(morphableTargets.get(0).isMorphable);
assertFalse(morphableTargets.get(1).isMorphable);
assertTrue(morphableTargets.get(302).isMorphable);
assertTrue(morphableTargets.get(303).isMorphable);

try {
synthesizer.morphableTargets(2);
} catch (StyleNotFoundException e) {
}
}

boolean checkAllMoras(
List<AccentPhrase> accentPhrases,
List<AccentPhrase> otherAccentPhrases,
Expand Down Expand Up @@ -152,4 +120,109 @@ void checkTts() throws InferenceFailedException, InvalidModelDataException {
synthesizer.loadVoiceModel(model);
synthesizer.tts("こんにちは", model.metas[0].styles[0].id);
}

@ParameterizedTest
@MethodSource("morphParamsProvider")
void checkMorphing(MorphParams params)
throws InvalidModelDataException, InferenceFailedException {
OpenJtalk openJtalk = loadOpenJtalk();
Synthesizer synthesizer =
Synthesizer.builder(openJtalk).accelerationMode(Synthesizer.AccelerationMode.CPU).build();

synthesizer.loadVoiceModel(loadModel());

int baseStyleId = params.getBaseStyleId();
AudioQuery query = synthesizer.createAudioQuery("こんにちは", baseStyleId);
Map<Integer, MorphableTargetInfo> morphableTargets = synthesizer.morphableTargets(baseStyleId);

for (Map.Entry<Integer, Boolean> entry : params.getTargets().entrySet()) {
int targetStyleId = entry.getKey();
boolean shouldSuccess = entry.getValue();

assertTrue(morphableTargets.get(targetStyleId).isMorphable == shouldSuccess);

try {
// TODO: スナップショットテストをやる
synthesizer.synthesisMorphing(query, baseStyleId, targetStyleId, 0.5);
assertTrue(shouldSuccess);
} catch (SpeakerFeatureException e) {
assertFalse(shouldSuccess);
}
}
}

static Stream<MorphParams> morphParamsProvider() {
return Stream.of(
new MorphParams(
0,
new TreeMap<Integer, Boolean>() {
{
put(0, false);
put(1, false);
put(302, false);
put(303, false);
}
}),
new MorphParams(
1,
new TreeMap<Integer, Boolean>() {
{
put(0, false);
put(1, true);
put(302, false);
put(303, false);
}
}),
new MorphParams(
302,
new TreeMap<Integer, Boolean>() {
{
put(0, false);
put(1, false);
put(302, true);
put(303, true);
}
}),
new MorphParams(
303,
new TreeMap<Integer, Boolean>() {
{
put(0, false);
put(1, false);
put(302, true);
put(303, true);
}
}));
}

// TODO: Lombokを使う
static class MorphParams {
private final int baseStyleId;
private final SortedMap<Integer, Boolean> targets;

MorphParams(int baseStyleId, SortedMap<Integer, Boolean> targets) {
this.baseStyleId = baseStyleId;
this.targets = targets;
}

int getBaseStyleId() {
return baseStyleId;
}

SortedMap<Integer, Boolean> getTargets() {
return targets;
}
}

@Test
void checkMorphableTargetsDeniesUnknownStyle() {
OpenJtalk openJtalk = loadOpenJtalk();
Synthesizer synthesizer =
Synthesizer.builder(openJtalk).accelerationMode(Synthesizer.AccelerationMode.CPU).build();

try {
synthesizer.morphableTargets(0);
} catch (StyleNotFoundException e) {
}
}
}
110 changes: 110 additions & 0 deletions crates/voicevox_core_python_api/python/test/test_asyncio_morph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
モーフィング機能をテストする。
``test_blocking_morph`` と対になる。
"""

from typing import Dict

import conftest
import pytest
import pytest_asyncio
from voicevox_core import SpeakerFeatureError, StyleId, StyleNotFoundError
from voicevox_core.asyncio import OpenJtalk, Synthesizer, VoiceModel


@pytest.mark.asyncio
@pytest.mark.parametrize(
"base, targets",
[
(
0,
{
0: False,
1: False,
302: False,
303: False,
},
),
(
1,
{
0: False,
1: True,
302: False,
303: False,
},
),
(
302,
{
0: False,
1: False,
302: True,
303: True,
},
),
(
303,
{
0: False,
1: False,
302: True,
303: True,
},
),
],
)
async def test_morph(
synthesizer: Synthesizer, base: StyleId, targets: Dict[StyleId, bool]
) -> None:
TEXT = "こんにちは"
MORPH_RATE = 0.5

query = await synthesizer.audio_query(TEXT, base)

for target, should_success in targets.items():
is_morphable = synthesizer.morphable_targets(base)[target].is_morphable
assert is_morphable == should_success

if should_success:
# TODO: スナップショットテストをやる
await synthesizer.synthesis_morphing(query, base, target, MORPH_RATE)
else:
with pytest.raises(
SpeakerFeatureError,
match=(
r"^`dummy[1-3]` \([0-9a-f-]{36}\)は以下の機能を持ちません: "
r"`dummy[1-3]` \([0-9a-f-]{36}\)に対するモーフィング$"
),
):
await synthesizer.synthesis_morphing(query, base, target, MORPH_RATE)


def test_morphable_targets_raises_for_unknown_style(synthesizer: Synthesizer) -> None:
STYLE_ID = StyleId(9999)

# FIXME: `KeyError.__init__`を通しているため、メッセージが`repr`で表示されてしまう
# https://github.com/VOICEVOX/voicevox_core/blob/4e13bca5a55a08d7aea08af4f949462bd284b1c1/crates/voicevox_core_python_api/src/convert.rs#L186-L206
with pytest.raises(
StyleNotFoundError,
match=f"^'`{STYLE_ID}`に対するスタイルが見つかりませんでした。音声モデルが読み込まれていないか、読み込みが解除されています'$",
):
synthesizer.morphable_targets(STYLE_ID)


@pytest_asyncio.fixture
async def synthesizer(open_jtalk: OpenJtalk, model: VoiceModel) -> Synthesizer:
synthesizer = Synthesizer(open_jtalk)
await synthesizer.load_voice_model(model)
return synthesizer


@pytest_asyncio.fixture
async def open_jtalk() -> OpenJtalk:
return await OpenJtalk.new(conftest.open_jtalk_dic_dir)


@pytest_asyncio.fixture
async def model() -> VoiceModel:
return await VoiceModel.from_path(conftest.model_dir)
Loading

0 comments on commit 9c41398

Please sign in to comment.