Skip to content

Commit

Permalink
モーフィング関係で、スタイルIDなのにspeakerになっているところをstyle_idに変更 (#826)
Browse files Browse the repository at this point in the history
* モーフィング関係で、スタイルIDなのにspeakerになっているところをstyle_idに変更

* NOTE化

* run.pyのとこStyleIdに

* API引数をStyleId化

* 間違えて追加してしまっていた

* get_style_id_from_deprecated周りの変更

* audio_queryのe2eテスト

* stash

* 漏れ

* さらなる漏れの修正

* StyleIdの場所移動

* StyleIdの場所変更

* pysen

* 自動import箇所が意図とあってなさそうだった

* なぜか再代入しても大丈夫なようになっていた。。。

* to depreceated_speaker

* タイポ。。
  • Loading branch information
Hiroshiba authored Jan 6, 2024
1 parent c14f131 commit 0e4baea
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 63 deletions.
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ curl -s \
### 読み方を AquesTalk 風記法で取得・修正

#### AquesTalk 風記法

<!-- NOTE: この節は静的リンクとして運用中なので変更しない方が良い(voicevox_engine#816) -->

**AquesTalk 風記法**」はカタカナと記号だけで読み方を指定する記法です。[AquesTalk 本家の記法](https://www.a-quest.com/archive/manual/siyo_onseikigou.pdf)とは一部が異なります。
Expand All @@ -90,7 +91,7 @@ AquesTalk 風記法は次のルールに従います:
#### AquesTalk 風記法のサンプルコード

`/audio_query`のレスポンスにはエンジンが判断した読み方が[AquesTalk 風記法](#aquestalk-風記法)で記述されます。
これを修正することで音声の読み仮名やアクセントを制御できます。
これを修正することで音声の読み仮名やアクセントを制御できます。

```bash
# 読ませたい文章をutf-8でtext.txtに書き出す
Expand Down Expand Up @@ -245,20 +246,20 @@ curl -s \
- `id`は重複してはいけません
- エンジン起動後にファイルを書き換えるとエンジンに反映されます

### 2 人の話者でモーフィングするサンプルコード
### 2 種類のスタイルでモーフィングするサンプルコード

`/synthesis_morphing`では、2 人の話者でそれぞれ合成された音声を元に、モーフィングした音声を生成します。
`/synthesis_morphing`では、2 種類のスタイルでそれぞれ合成された音声を元に、モーフィングした音声を生成します。

```bash
echo -n "モーフィングを利用することで、2つの声を混ぜることができます" > text.txt
echo -n "モーフィングを利用することで、2種類の声を混ぜることができます" > text.txt

curl -s \
-X POST \
"127.0.0.1:50021/audio_query?style_id=0"\
--get --data-urlencode [email protected] \
> query.json

# 元の話者での合成結果
# 元のスタイルでの合成結果
curl -s \
-H "Content-Type: application/json" \
-X POST \
Expand All @@ -268,22 +269,22 @@ curl -s \

export MORPH_RATE=0.5

# 話者2人分の音声合成+WORLDによる音声分析が入るため時間が掛かるので注意
# スタイル2種類分の音声合成+WORLDによる音声分析が入るため時間が掛かるので注意
curl -s \
-H "Content-Type: application/json" \
-X POST \
-d @query.json \
"127.0.0.1:50021/synthesis_morphing?base_speaker=0&target_speaker=1&morph_rate=$MORPH_RATE" \
"127.0.0.1:50021/synthesis_morphing?base_style_id=0&target_style_id=1&morph_rate=$MORPH_RATE" \
> audio.wav

export MORPH_RATE=0.9

# query、base_speaker、target_speakerが同じ場合はキャッシュが使用されるため比較的高速に生成される
# query、base_style_id、target_style_idが同じ場合はキャッシュが使用されるため比較的高速に生成される
curl -s \
-H "Content-Type: application/json" \
-X POST \
-d @query.json \
"127.0.0.1:50021/synthesis_morphing?base_speaker=0&target_speaker=1&morph_rate=$MORPH_RATE" \
"127.0.0.1:50021/synthesis_morphing?base_style_id=0&target_style_id=1&morph_rate=$MORPH_RATE" \
> audio.wav
```

Expand Down
2 changes: 1 addition & 1 deletion engine_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"synthesis_morphing" : {
"type": "bool",
"value": true,
"name": "2人の話者でモーフィングした音声を合成"
"name": "2種類のスタイルでモーフィングした音声を合成"
},
"manage_library": {
"type": "bool",
Expand Down
96 changes: 63 additions & 33 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from io import BytesIO, TextIOWrapper
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryFile
from typing import Annotated, Any, Optional
from typing import Annotated, Any, Optional, TypeVar

import soundfile
import uvicorn
Expand Down Expand Up @@ -91,18 +91,19 @@
)
from voicevox_engine.utility.run_utility import decide_boolean_from_env

# NOTE: Python 3.12以降で[S: StyleId | list[StyleId]]に置き換えられる
S = TypeVar("S", StyleId, list[StyleId])

def get_style_id_from_deprecated(
style_id: StyleId | None, speaker_id: StyleId | None
) -> StyleId:

def get_style_id_from_deprecated(style_id: S | None, deprecated_speaker: S | None) -> S:
"""
style_idとspeaker_id両方ともNoneかNoneでないかをチェックし
style_idとspeaker両方ともNoneかNoneでないかをチェックし
どちらか片方しかNoneが存在しなければstyle_idを返す
"""
if speaker_id is not None and style_id is None:
if deprecated_speaker is not None and style_id is None:
warnings.warn("speakerは非推奨です。style_idを利用してください。", stacklevel=1)
return speaker_id
elif style_id is not None and speaker_id is None:
return deprecated_speaker
elif style_id is not None and deprecated_speaker is None:
return style_id
raise HTTPException(
status_code=400, detail="speakerとstyle_idが両方とも存在しないか、両方とも存在しています。"
Expand Down Expand Up @@ -289,7 +290,9 @@ def audio_query(
"""
音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。
"""
style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker)
style_id = get_style_id_from_deprecated(
style_id=style_id, deprecated_speaker=speaker
)
engine = get_engine(core_version)
core = get_core(core_version)
accent_phrases = engine.create_accent_phrases(text, style_id)
Expand Down Expand Up @@ -375,7 +378,9 @@ def accent_phrases(
* アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を1つ指定する必要がある。
* アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる。
"""
style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker)
style_id = get_style_id_from_deprecated(
style_id=style_id, deprecated_speaker=speaker
)
engine = get_engine(core_version)
if is_kana:
try:
Expand Down Expand Up @@ -403,7 +408,9 @@ def mora_data(
speaker: StyleId | None = Query(default=None, deprecated=True), # noqa: B008
core_version: str | None = None,
) -> list[AccentPhrase]:
style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker)
style_id = get_style_id_from_deprecated(
style_id=style_id, deprecated_speaker=speaker
)
engine = get_engine(core_version)
return engine.update_length_and_pitch(accent_phrases, style_id)

Expand All @@ -419,7 +426,9 @@ def mora_length(
speaker: StyleId | None = Query(default=None, deprecated=True), # noqa: B008
core_version: str | None = None,
) -> list[AccentPhrase]:
style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker)
style_id = get_style_id_from_deprecated(
style_id=style_id, deprecated_speaker=speaker
)
engine = get_engine(core_version)
return engine.update_length(accent_phrases, style_id)

Expand All @@ -435,7 +444,9 @@ def mora_pitch(
speaker: StyleId | None = Query(default=None, deprecated=True), # noqa: B008
core_version: str | None = None,
) -> list[AccentPhrase]:
style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker)
style_id = get_style_id_from_deprecated(
style_id=style_id, deprecated_speaker=speaker
)
engine = get_engine(core_version)
return engine.update_pitch(accent_phrases, style_id)

Expand All @@ -462,7 +473,9 @@ def synthesis(
),
core_version: str | None = None,
) -> FileResponse:
style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker)
style_id = get_style_id_from_deprecated(
style_id=style_id, deprecated_speaker=speaker
)
engine = get_engine(core_version)
wave = engine.synthesize_wave(
query, style_id, enable_interrogative_upspeak=enable_interrogative_upspeak
Expand Down Expand Up @@ -499,7 +512,9 @@ def cancellable_synthesis(
speaker: StyleId | None = Query(default=None, deprecated=True), # noqa: B008
core_version: str | None = None,
) -> FileResponse:
style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker)
style_id = get_style_id_from_deprecated(
style_id=style_id, deprecated_speaker=speaker
)
if cancellable_engine is None:
raise HTTPException(
status_code=404,
Expand Down Expand Up @@ -538,7 +553,9 @@ def multi_synthesis(
speaker: StyleId | None = Query(default=None, deprecated=True), # noqa: B008
core_version: str | None = None,
) -> FileResponse:
style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker)
style_id = get_style_id_from_deprecated(
style_id=style_id, deprecated_speaker=speaker
)
engine = get_engine(core_version)
sampling_rate = queries[0].outputSamplingRate

Expand Down Expand Up @@ -571,24 +588,28 @@ def multi_synthesis(
"/morphable_targets",
response_model=list[dict[str, MorphableTargetInfo]],
tags=["音声合成"],
summary="指定した話者に対してエンジン内の話者がモーフィングが可能か判定する",
summary="指定したスタイルに対してエンジン内の話者がモーフィングが可能か判定する",
)
def morphable_targets(
base_speakers: list[int], # FIXME: StyleId型にする
base_style_ids: list[StyleId] | None = Query(default=None), # noqa: B008
base_speakers: list[StyleId] | None = Query(default=None), # noqa: B008
core_version: str | None = None,
) -> list[dict[str, MorphableTargetInfo]]:
"""
指定されたベース話者に対してエンジン内の各話者がモーフィング機能を利用可能か返します
指定されたベーススタイルに対してエンジン内の各話者がモーフィング機能を利用可能か返します
モーフィングの許可/禁止は`/speakers`の`speaker.supported_features.synthesis_morphing`に記載されています。
プロパティが存在しない場合は、モーフィングが許可されているとみなします。
返り値の話者はstring型なので注意。
"""
base_style_ids = get_style_id_from_deprecated(
style_id=base_style_ids, deprecated_speaker=base_speakers
)
core = get_core(core_version)

try:
speakers = metas_store.load_combined_metas(core=core)
morphable_targets = get_morphable_targets(
speakers=speakers, base_speakers=base_speakers
speakers=speakers, base_style_ids=base_style_ids
)
# jsonはint型のキーを持てないので、string型に変換する
return [
Expand All @@ -611,27 +632,39 @@ def morphable_targets(
}
},
tags=["音声合成"],
summary="2人の話者でモーフィングした音声を合成する",
summary="2種類のスタイルでモーフィングした音声を合成する",
)
def _synthesis_morphing(
query: AudioQuery,
base_speaker: int, # FIXME: StyleId型にする
target_speaker: int,
base_style_id: StyleId | None = Query(default=None), # noqa: B008
base_speaker: (StyleId | None) = Query( # noqa: B008
default=None, deprecated=True
),
target_style_id: StyleId | None = Query(default=None), # noqa: B008
target_speaker: (StyleId | None) = Query( # noqa: B008
default=None, deprecated=True
),
morph_rate: float = Query(..., ge=0.0, le=1.0), # noqa: B008
core_version: str | None = None,
) -> FileResponse:
"""
指定された2人の話者で音声を合成、指定した割合でモーフィングした音声を得ます。
モーフィングの割合は`morph_rate`で指定でき、0.0でベースの話者、1.0でターゲットの話者に近づきます
指定された2種類のスタイルで音声を合成、指定した割合でモーフィングした音声を得ます。
モーフィングの割合は`morph_rate`で指定でき、0.0でベースのスタイル、1.0でターゲットのスタイルに近づきます
"""
base_style_id = get_style_id_from_deprecated(
style_id=base_style_id, deprecated_speaker=base_speaker
)
target_style_id = get_style_id_from_deprecated(
style_id=target_style_id, deprecated_speaker=target_speaker
)
engine = get_engine(core_version)
core = get_core(core_version)

try:
speakers = metas_store.load_combined_metas(core=core)
speaker_lookup = construct_lookup(speakers=speakers)
is_permitted = is_synthesis_morphing_permitted(
speaker_lookup, base_speaker, target_speaker
speaker_lookup, base_style_id, target_style_id
)
if not is_permitted:
raise HTTPException(
Expand All @@ -648,8 +681,8 @@ def _synthesis_morphing(
engine=engine,
core=core,
query=query,
base_speaker=base_speaker,
target_speaker=target_speaker,
base_style_id=base_style_id,
target_style_id=target_style_id,
)

morph_wave = synthesis_morphing(
Expand Down Expand Up @@ -1033,9 +1066,7 @@ def initialize_speaker(
core_version: str | None = None,
) -> Response:
"""
こちらのAPIは非推奨です。`initialize_style_id`を利用してください。\n
指定されたspeaker_idの話者を初期化します。
実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。
こちらのAPIは非推奨です。`initialize_style_id`を利用してください。
"""
warnings.warn(
"使用しているAPI(/initialize_speaker)は非推奨です。/initialized_style_idを利用してください。",
Expand All @@ -1053,8 +1084,7 @@ def is_initialized_speaker(
core_version: str | None = None,
) -> bool:
"""
こちらのAPIは非推奨です。`is_initialize_style_id`を利用してください。\n
指定されたspeaker_idの話者が初期化されているかどうかを返します。
こちらのAPIは非推奨です。`is_initialize_style_id`を利用してください。
"""
warnings.warn(
"使用しているAPI(/is_initialize_speaker)は非推奨です。/is_initialized_style_idを利用してください。",
Expand Down
2 changes: 1 addition & 1 deletion voicevox_engine/engine_manifest/EngineManifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class SupportedFeatures(BaseModel):
adjust_intonation_scale: bool = Field(title="全体の抑揚の調整")
adjust_volume_scale: bool = Field(title="全体の音量の調整")
interrogative_upspeak: bool = Field(title="疑問文の自動調整")
synthesis_morphing: bool = Field(title="2人の話者でモーフィングした音声を合成")
synthesis_morphing: bool = Field(title="2種類のスタイルでモーフィングした音声を合成")
manage_library: Optional[bool] = Field(title="音声ライブラリのインストール・アンインストール")


Expand Down
7 changes: 4 additions & 3 deletions voicevox_engine/metas/MetasStore.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
EngineSpeaker,
Speaker,
SpeakerStyle,
StyleId,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -61,7 +62,7 @@ def load_combined_metas(self, core: "CoreAdapter") -> List[Speaker]:

def construct_lookup(
speakers: List[Speaker],
) -> Dict[int, Tuple[Speaker, SpeakerStyle]]:
) -> Dict[StyleId, Tuple[Speaker, SpeakerStyle]]:
"""
スタイルID に話者メタ情報・スタイルメタ情報を紐付ける対応表を生成
Parameters
Expand All @@ -70,10 +71,10 @@ def construct_lookup(
話者メタ情報
Returns
-------
ret : Dict[int, Tuple[Speaker, SpeakerStyle]]
ret : Dict[StyleId, Tuple[Speaker, SpeakerStyle]]
スタイルID に話者メタ情報・スタイルメタ情報が紐付いた対応表
"""
lookup_table: dict[int, tuple[Speaker, SpeakerStyle]] = dict()
lookup_table: dict[StyleId, tuple[Speaker, SpeakerStyle]] = dict()
for speaker in speakers:
for style in speaker.styles:
lookup_table[style.id] = (speaker, style)
Expand Down
Loading

0 comments on commit 0e4baea

Please sign in to comment.