-
Notifications
You must be signed in to change notification settings - Fork 4
/
morphing.py
137 lines (123 loc) · 6.1 KB
/
morphing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""モーフィング機能を提供する API Router"""
import io
from functools import lru_cache
from typing import Annotated
import soundfile
from fastapi import APIRouter, HTTPException, Query, Response
from pydantic.json_schema import SkipJsonSchema
from voicevox_engine.aivm_manager import AivmManager
from voicevox_engine.metas.Metas import StyleId
from voicevox_engine.model import AudioQuery
from voicevox_engine.morphing.model import MorphableTargetInfo
from voicevox_engine.morphing.morphing import (
StyleIdNotFoundError,
get_morphable_targets,
is_morphable,
)
from voicevox_engine.morphing.morphing import (
synthesis_morphing_parameter as _synthesis_morphing_parameter,
)
from voicevox_engine.morphing.morphing import synthesize_morphed_wave
from voicevox_engine.tts_pipeline.tts_engine import LATEST_VERSION, TTSEngineManager
# キャッシュを有効化
# モジュール側でlru_cacheを指定するとキャッシュを制御しにくいため、HTTPサーバ側で指定する
# TODO: キャッシュを管理するモジュール側API・HTTP側APIを用意する
synthesis_morphing_parameter = lru_cache(maxsize=4)(_synthesis_morphing_parameter)
def generate_morphing_router(
tts_engines: TTSEngineManager,
aivm_manager: AivmManager,
) -> APIRouter:
"""モーフィング API Router を生成する"""
router = APIRouter(tags=["音声合成"])
@router.post(
"/morphable_targets",
summary="指定したスタイルに対してエンジン内のキャラクターがモーフィングが可能か判定する",
)
def morphable_targets(
base_style_ids: list[StyleId],
core_version: Annotated[
str | SkipJsonSchema[None],
Query(description="AivisSpeech Engine ではサポートされていないパラメータです (常に無視されます) 。"),
] = None, # fmt: skip # noqa
) -> list[dict[str, MorphableTargetInfo]]:
"""
指定されたベーススタイルに対してエンジン内の各キャラクターがモーフィング機能を利用可能か返します。<br>
モーフィングの許可/禁止は `/speakers `の `speaker.supported_features.synthesis_morphing` に記載されています。<br>
プロパティが存在しない場合は、モーフィングが許可されているとみなします。<br>
返り値のスタイル ID は string 型なので注意。<br>
AivisSpeech Engine では話者ごとに発声タイミングが異なる関係で実装不可能なため (動作こそするが聴くに耐えない) 、
全ての話者でモーフィングが禁止されています。
""" # noqa
characters = aivm_manager.get_characters()
try:
morphable_targets = get_morphable_targets(characters, base_style_ids)
except StyleIdNotFoundError as e:
msg = f"該当するスタイル(style_id={e.style_id})が見つかりません"
raise HTTPException(status_code=404, detail=msg)
# NOTE: jsonはint型のキーを持てないので、string型に変換する
return [
{str(k): v for k, v in morphable_target.items()}
for morphable_target in morphable_targets
]
@router.post(
"/synthesis_morphing",
response_class=Response,
responses={
200: {
"content": {
"audio/wav": {"schema": {"type": "string", "format": "binary"}}
},
}
},
summary="2種類のスタイルでモーフィングした音声を合成する",
)
def _synthesis_morphing(
query: AudioQuery,
base_style_id: Annotated[StyleId, Query(alias="base_speaker")],
target_style_id: Annotated[StyleId, Query(alias="target_speaker")],
morph_rate: Annotated[float, Query(ge=0.0, le=1.0)],
core_version: Annotated[
str | SkipJsonSchema[None],
Query(description="AivisSpeech Engine ではサポートされていないパラメータです (常に無視されます) 。"),
] = None, # fmt: skip # noqa
) -> Response:
"""
指定された 2 種類のスタイルで音声を合成、指定した割合でモーフィングした音声を得ます。<br>
モーフィングの割合は `morph_rate` で指定でき、0.0 でベースのスタイル、1.0 でターゲットのスタイルに近づきます。<br>
AivisSpeech Engine では話者ごとに発声タイミングが異なる関係で実装不可能なため (動作こそするが聴くに耐えない) 、
常に 400 Bad Request を返します。
"""
version = core_version or LATEST_VERSION
engine = tts_engines.get_engine(version)
# モーフィングが許可されないキャラクターペアを拒否する
characters = aivm_manager.get_characters()
try:
morphable = is_morphable(characters, base_style_id, target_style_id)
except StyleIdNotFoundError as e:
msg = f"該当するスタイル(style_id={e.style_id})が見つかりません"
raise HTTPException(status_code=404, detail=msg)
if not morphable:
msg = "指定されたスタイルペアでのモーフィングはできません"
raise HTTPException(status_code=400, detail=msg)
# 生成したパラメータはキャッシュされる
morph_param = synthesis_morphing_parameter(
engine=engine,
query=query,
base_style_id=base_style_id,
target_style_id=target_style_id,
)
morph_wave = synthesize_morphed_wave(
morph_param=morph_param,
morph_rate=morph_rate,
output_fs=query.outputSamplingRate,
output_stereo=query.outputStereo,
)
buffer = io.BytesIO()
soundfile.write(
file=buffer,
data=morph_wave,
samplerate=query.outputSamplingRate,
format="WAV",
)
return Response(buffer.getvalue(), media_type="audio/wav")
return router