From 26f4800a179bb4c4ababf4a309c1350fab9d7ba0 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 15 May 2024 18:00:02 +0900 Subject: [PATCH 01/82] =?UTF-8?q?ENH:=20`speaker=5Finfo`=E3=81=AE=E3=83=AA?= =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=82=B9=E3=82=92URL=E3=81=A7=E8=BF=94?= =?UTF-8?q?=E3=81=99=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_util/file_map.py | 63 +++++++++ engine_manifest.json | 5 + speaker_info/filemap.json | 60 ++++++++ ...\343\202\222\347\242\272\350\252\215.json" | 24 ++++ .../test_get_engine_manifest_200.json | 1 + voicevox_engine/app/routers/speaker.py | 131 +++++++++++++++--- .../engine_manifest/EngineManifest.py | 3 + voicevox_engine/metas/MetasStore.py | 1 + 8 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 build_util/file_map.py create mode 100644 speaker_info/filemap.json diff --git a/build_util/file_map.py b/build_util/file_map.py new file mode 100644 index 000000000..412512cfd --- /dev/null +++ b/build_util/file_map.py @@ -0,0 +1,63 @@ +import json +import os +from argparse import ArgumentParser +from collections.abc import Generator +from hashlib import md5 +from pathlib import Path, PurePosixPath + +SPEAKER_INFO_DIR = (Path(__file__).parents[1] / "speaker_info").resolve() +DEFAULT_FILENAME = "filemap.json" + + +def to_posix_str_path(path: Path) -> str: + return str(PurePosixPath(path)) + + +def make_hash(file: Path) -> str: + digest = md5(file.read_bytes()).digest() + return digest.hex() + + +def walk_character_files( + character_dir: Path, suffix: tuple[str, ...] +) -> Generator[tuple[Path, str], None, None]: + for root, _, files in os.walk(character_dir): + for file in files: + filepath = Path(root, file) + if not filepath.suffix.endswith(suffix): + continue + filehash = make_hash(filepath) + relative = filepath.relative_to(character_dir) + yield (relative, filehash) + + +def mapping(target: Path) -> dict[str, dict[str, str]]: + return { + character_dir.name: { + to_posix_str_path(filepath): filehash + for filepath, filehash in walk_character_files( + character_dir, ("wav", "png") + ) + } + for character_dir in target.iterdir() + if character_dir.is_dir() + } + + +def main() -> None: + parser = ArgumentParser() + parser.add_argument("--save", default=None, type=Path) + parser.add_argument("--target", default=SPEAKER_INFO_DIR, type=Path) + arg = parser.parse_args() + save: Path = arg.target if arg.save is None else arg.save + save_file: Path = save if not save.is_dir() else save / DEFAULT_FILENAME + target_dir: Path = arg.target + if not target_dir.is_dir(): + raise Exception() + maped = mapping(target_dir) + with save_file.open(mode="wt", encoding="utf-8") as f: + json.dump(maped, f, ensure_ascii=False) + + +if __name__ == "__main__": + main() diff --git a/engine_manifest.json b/engine_manifest.json index a422a78bc..fd9a3b6e8 100644 --- a/engine_manifest.json +++ b/engine_manifest.json @@ -63,6 +63,11 @@ "type": "bool", "value": true, "name": "音声ライブラリのインストール・アンインストール" + }, + "resource_url":{ + "type":"bool", + "value":true, + "name": "speaker_info・singer_infoのリソースをURLで返す" } } } diff --git a/speaker_info/filemap.json b/speaker_info/filemap.json new file mode 100644 index 000000000..2df6ab345 --- /dev/null +++ b/speaker_info/filemap.json @@ -0,0 +1,60 @@ +{ + "35b2c544-660e-401e-b503-0e14c635303a": { + "portrait.png": "bd6cf66dcc652f56892b14b423f6f37c", + "icons/8.png": "1fe576e75458c752cfeecc1e93a29886", + "portraits/8.png": "56b2cfc4a9118c40a999e151c0ac647e", + "voice_samples/8_001.wav": "62cce06e564276499df6014e7182368d", + "voice_samples/8_002.wav": "2164af6fc692d5b2117dfd845c880f81", + "voice_samples/8_003.wav": "4e81220a91745cf2ab7b632cd528ffbd" + }, + "388f246b-8c41-4ac1-8e2d-5d79f3ff56d9": { + "portrait.png": "27777cb0883c98cd9870707005bf1faf", + "icons/1.png": "becb1cc2aaf82623a13a1250a39d7393", + "icons/3.png": "9a3690c368cd9a4ecb1940ff9eb2c955", + "icons/5.png": "517ba089e0b03f8868af2ce956f7699d", + "icons/7.png": "562ad0f61ca6dd81e89a4479f97dcd9f", + "portraits/3.png": "0308b1a8e7a849e8be5ea699706f5097", + "voice_samples/1_001.wav": "a9bf75355816d858213cb116942fe499", + "voice_samples/1_002.wav": "0dc81612c1f305b6210ef325a4518e53", + "voice_samples/1_003.wav": "ba5694044d8e7e0bffa9578d22ba2ba8", + "voice_samples/3_001.wav": "0fd8a039030ea31560c84e91f955e4cd", + "voice_samples/3_002.wav": "c249264fa985fd4ab5e940c7e813db3e", + "voice_samples/3_003.wav": "6a0bc8b54543fe816f37cb286795ad07", + "voice_samples/5_001.wav": "d368ea4f7af3fba9f9f7fa862e50590f", + "voice_samples/5_002.wav": "a844eb96e25efd52dfee76d023eda0c8", + "voice_samples/5_003.wav": "4cd9a6ff5ac76ea1c267c99b2cfe925f", + "voice_samples/7_001.wav": "f214d07c3fb5332e429abb17921eecb5", + "voice_samples/7_002.wav": "46296628b586d968054cc43ad733dd4a", + "voice_samples/7_003.wav": "919e8e440b31b13fb0d2aae03061661f" + }, + "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff": { + "portrait.png": "78228648fe527ee23597b66db59a4f94", + "icons/0.png": "1f1da5f25968c638a783bf6ba9df9420", + "icons/2.png": "0b158046338f60a53e9afdb7797c5864", + "icons/4.png": "c6a8ddea789d8115372db31b4a76d2aa", + "icons/6.png": "d2c7c85a9919372ef62ad49786bd7fc4", + "portraits/0.png": "1724b3741e58978f5b9db25eb6575d9b", + "portraits/2.png": "593c75329b6531e5ae0266a708e5ebc0", + "portraits/4.png": "2b339e97d7ae0b6b215de842ff12515e", + "portraits/6.png": "6f7cd8ecb9d2d8de0ed410e05350ac72", + "voice_samples/0_001.wav": "79d52a44a8dc8548616c300e49b37a94", + "voice_samples/0_002.wav": "d31e59cf938a46d156add0da486fe5e2", + "voice_samples/0_003.wav": "73d7e1f311263ab7e1b468318cb8575a", + "voice_samples/2_001.wav": "8e917c07fe5d444eddd4b451b7dc8d83", + "voice_samples/2_002.wav": "0cf95e313bc5c3314be6a71e5021fad2", + "voice_samples/2_003.wav": "43f9f43e4607015e7ab3bd016bee5509", + "voice_samples/4_001.wav": "5333edce35b3806f1818c407cd74b66b", + "voice_samples/4_002.wav": "720ab16616e8e3e8e15535c9f6fd1f03", + "voice_samples/4_003.wav": "444dd9f019828f3773192cca7d1099df", + "voice_samples/6_001.wav": "4add3a1cf25c4b6ce6d35aaa1f760071", + "voice_samples/6_002.wav": "42d20380d8d6fc95916037e4da990aaa", + "voice_samples/6_003.wav": "af22bcd03f4959dc8f0e4ac1d04854e0" + }, + "b1a81618-b27b-40d2-b0ea-27a9ad408c4b": { + "portrait.png": "381ba07cbcad95f1c99109f5f6d096a8", + "icons/9.png": "1e3d0a6e88264b1b99839aeb7c7cbe1f", + "voice_samples/9_001.wav": "e8626456ba706690676639e61ad43b73", + "voice_samples/9_002.wav": "93e3d5c44bbad3ddfc43953f5c751670", + "voice_samples/9_003.wav": "765ff637563e64182ace61fb5ca7daf4" + } +} diff --git "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" index 24a6df020..de6e6f694 100644 --- "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" +++ "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" @@ -875,6 +875,10 @@ "title": "音声ライブラリのインストール・アンインストール", "type": "boolean" }, + "resource_url": { + "title": "speaker_info・singer_infoのリソースをURLで返す", + "type": "boolean" + }, "sing": { "title": "歌唱音声合成", "type": "boolean" @@ -2450,6 +2454,16 @@ "type": "string" } }, + { + "in": "query", + "name": "resource_url", + "required": false, + "schema": { + "default": false, + "title": "Resource Url", + "type": "boolean" + } + }, { "in": "query", "name": "core_version", @@ -2548,6 +2562,16 @@ "type": "string" } }, + { + "in": "query", + "name": "resource_url", + "required": false, + "schema": { + "default": false, + "title": "Resource Url", + "type": "boolean" + } + }, { "in": "query", "name": "core_version", diff --git a/test/e2e/single_api/engine_info/__snapshots__/test_engine_manifest/test_get_engine_manifest_200.json b/test/e2e/single_api/engine_info/__snapshots__/test_engine_manifest/test_get_engine_manifest_200.json index 78bde9b13..f8d096796 100644 --- a/test/e2e/single_api/engine_info/__snapshots__/test_engine_manifest/test_get_engine_manifest_200.json +++ b/test/e2e/single_api/engine_info/__snapshots__/test_engine_manifest/test_get_engine_manifest_200.json @@ -22,6 +22,7 @@ "adjust_volume_scale": true, "interrogative_upspeak": true, "manage_library": true, + "resource_url": true, "sing": true, "synthesis_morphing": true }, diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 834db34a9..1618f4156 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -2,10 +2,12 @@ import base64 import json +from os.path import basename from pathlib import Path from typing import Annotated, Literal -from fastapi import APIRouter, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response +from fastapi.responses import FileResponse from pydantic import parse_obj_as from voicevox_engine.core.core_initializer import CoreManager @@ -14,10 +16,41 @@ from voicevox_engine.model import Speaker, SpeakerInfo +async def get_character_resource_baseurl(request: Request) -> str: + return f"{request.url.scheme}://{request.url.netloc}/character_resources" + + def b64encode_str(s: bytes) -> str: return base64.b64encode(s).decode("utf-8") +def read_mapfile(speaker_info_dir: Path) -> dict[str, dict[str, str]]: + filemap_path = speaker_info_dir / "filemap.json" + with filemap_path.open(mode="rb") as f: + filemap: dict[str, dict[str, str]] = json.load(f) + return filemap + + +def gen_file_name(filehash: str, filename: str) -> str: + return f"{filehash}_{filename}" + + +def file_url(base_url: str, speaker_id: str, filehash: str, filename: str) -> str: + return f"{base_url}/{speaker_id}/" + gen_file_name(filehash, filename) + + +def gen_hash_to_resource_id_mapping( + filemap: dict[str, dict[str, str]] +) -> dict[str, dict[str, str]]: + return { + speaker_uuid: { + f"{filehash}_{basename(filepath)}": filepath + for filepath, filehash in ph.items() + } + for speaker_uuid, ph in filemap.items() + } + + def generate_speaker_router( core_manager: CoreManager, metas_store: MetasStore, @@ -26,6 +59,8 @@ def generate_speaker_router( """話者情報 API Router を生成する""" router = APIRouter() + speaker_info_dir = root_dir / "speaker_info" + @router.get("/speakers", tags=["その他"]) def speakers( core_version: str | None = None, @@ -35,7 +70,9 @@ def speakers( @router.get("/speaker_info", tags=["その他"]) def speaker_info( + self_url: Annotated[str, Depends(get_character_resource_baseurl)], speaker_uuid: str, + resource_url: bool = False, core_version: str | None = None, ) -> SpeakerInfo: """ @@ -46,13 +83,19 @@ def speaker_info( speaker_uuid=speaker_uuid, speaker_or_singer="speaker", core_version=core_version, + self_url=self_url, + resource_url=resource_url, ) + mapfile = read_mapfile(speaker_info_dir) + # FIXME: この関数をどこかに切り出す def _speaker_info( speaker_uuid: str, speaker_or_singer: Literal["speaker", "singer"], core_version: str | None, + self_url: str, + resource_url: bool, ) -> SpeakerInfo: # エンジンに含まれる話者メタ情報は、次のディレクトリ構造に従わなければならない: # {root_dir}/ @@ -96,30 +139,62 @@ def _speaker_info( policy_path = speaker_path / "policy.md" policy = policy_path.read_text("utf-8") # speaker portrait - portrait_path = speaker_path / "portrait.png" - portrait = b64encode_str(portrait_path.read_bytes()) + if resource_url: + speaker_map = mapfile[speaker_uuid] + portrait = file_url( + self_url, + speaker_uuid, + speaker_map["portrait.png"], + "portrait.png", + ) + else: + portrait_path = speaker_path / "portrait.png" + portrait = b64encode_str(portrait_path.read_bytes()) # スタイル情報の取得 style_infos = [] for style in speaker.styles: id = style.id # style icon - style_icon_path = speaker_path / "icons" / f"{id}.png" - icon = b64encode_str(style_icon_path.read_bytes()) + style_icon_name = f"{id}.png" # style portrait - style_portrait_path = speaker_path / "portraits" / f"{id}.png" + style_portrait_name = f"{id}.png" style_portrait = None - if style_portrait_path.exists(): - style_portrait = b64encode_str(style_portrait_path.read_bytes()) # voice samples - voice_samples = [ - b64encode_str( - ( - speaker_path - / "voice_samples/{}_{}.wav".format(id, str(j + 1).zfill(3)) - ).read_bytes() - ) - for j in range(3) + voice_samples_names = [ + "{}_{}.wav".format(id, str(j + 1).zfill(3)) for j in range(3) ] + if resource_url: + icon_hash = speaker_map[f"icons/{style_icon_name}"] + icon = file_url(self_url, speaker_uuid, icon_hash, style_icon_name) + style_portrait_hash = speaker_map.get( + f"portraits/{style_portrait_name}" + ) + if style_portrait_hash is not None: + style_portrait = file_url( + self_url, speaker_uuid, style_portrait_hash, style_icon_name + ) + voice_samples_hashs = { + speaker_map[f"voice_samples/{name}"]: name + for name in voice_samples_names + } + voice_samples = [ + file_url(self_url, speaker_uuid, k, v) + for k, v in voice_samples_hashs.items() + ] + else: + style_icon_path = speaker_path / "icons" / style_icon_name + icon = b64encode_str(style_icon_path.read_bytes()) + style_portrait_path = ( + speaker_path / "portraits" / style_portrait_name + ) + if style_portrait_path.exists(): + style_portrait = b64encode_str(style_portrait_path.read_bytes()) + voice_samples = [ + b64encode_str( + (speaker_path / "voice_samples" / name).read_bytes() + ) + for name in voice_samples_names + ] style_infos.append( { "id": id, @@ -152,7 +227,9 @@ def singers( @router.get("/singer_info", tags=["その他"]) def singer_info( + self_url: Annotated[str, Depends(get_character_resource_baseurl)], speaker_uuid: str, + resource_url: bool = False, core_version: str | None = None, ) -> SpeakerInfo: """ @@ -163,7 +240,29 @@ def singer_info( speaker_uuid=speaker_uuid, speaker_or_singer="singer", core_version=core_version, + self_url=self_url, + resource_url=resource_url, + ) + + resource_id_mapping = gen_hash_to_resource_id_mapping(mapfile) + + @router.get( + "/character_resources/{speaker_uuid}/{resource_name}", include_in_schema=False + ) + async def static( + request: Request, speaker_uuid: str, resource_name: str + ) -> Response: + try: + resource = resource_id_mapping[speaker_uuid][resource_name] + except KeyError: + raise HTTPException(status_code=404) + headers = { + "Cache-Control": "max-age=2592000, immutable, stale-while-revalidate" + } + response = FileResponse( + speaker_info_dir / speaker_uuid / resource, headers=headers ) + return response @router.post("/initialize_speaker", status_code=204, tags=["その他"]) def initialize_speaker( diff --git a/voicevox_engine/engine_manifest/EngineManifest.py b/voicevox_engine/engine_manifest/EngineManifest.py index 2fa900c37..7efe064a9 100644 --- a/voicevox_engine/engine_manifest/EngineManifest.py +++ b/voicevox_engine/engine_manifest/EngineManifest.py @@ -50,6 +50,9 @@ class SupportedFeatures(BaseModel): manage_library: bool | None = Field( title="音声ライブラリのインストール・アンインストール" ) + resource_url: bool | None = Field( + title="speaker_info・singer_infoのリソースをURLで返す" + ) class EngineManifest(BaseModel): diff --git a/voicevox_engine/metas/MetasStore.py b/voicevox_engine/metas/MetasStore.py index d5655a6c2..f2287d08f 100644 --- a/voicevox_engine/metas/MetasStore.py +++ b/voicevox_engine/metas/MetasStore.py @@ -78,6 +78,7 @@ def __init__(self, engine_speakers_path: Path) -> None: **json.loads((folder / "metas.json").read_text(encoding="utf-8")) ) for folder in engine_speakers_path.iterdir() + if folder.is_dir() } # FIXME: engineではなくlist[CoreSpeaker]を渡す形にすることで From 8c58bab606d9673201222faf2102af02806a1596 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 24 May 2024 20:48:01 +0900 Subject: [PATCH 02/82] =?UTF-8?q?FIX:=20typo=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_util/file_map.py | 4 ++-- voicevox_engine/app/routers/speaker.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build_util/file_map.py b/build_util/file_map.py index 412512cfd..f7cf48390 100644 --- a/build_util/file_map.py +++ b/build_util/file_map.py @@ -54,9 +54,9 @@ def main() -> None: target_dir: Path = arg.target if not target_dir.is_dir(): raise Exception() - maped = mapping(target_dir) + mapped = mapping(target_dir) with save_file.open(mode="wt", encoding="utf-8") as f: - json.dump(maped, f, ensure_ascii=False) + json.dump(mapped, f, ensure_ascii=False) if __name__ == "__main__": diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 1618f4156..4fe2b139c 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -173,13 +173,13 @@ def _speaker_info( style_portrait = file_url( self_url, speaker_uuid, style_portrait_hash, style_icon_name ) - voice_samples_hashs = { + voice_samples_hashes = { speaker_map[f"voice_samples/{name}"]: name for name in voice_samples_names } voice_samples = [ file_url(self_url, speaker_uuid, k, v) - for k, v in voice_samples_hashs.items() + for k, v in voice_samples_hashes.items() ] else: style_icon_path = speaker_path / "icons" / style_icon_name From c565ffdedac8306c824655e13811b7007ad292a1 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 27 May 2024 20:23:37 +0900 Subject: [PATCH 03/82] =?UTF-8?q?FIX:=20=E3=82=B3=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 4fe2b139c..9ea8b5a78 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -246,6 +246,7 @@ def singer_info( resource_id_mapping = gen_hash_to_resource_id_mapping(mapfile) + # リソースはAPIとしてアクセスするものではないことを表明するためOpenAPIスキーマーから除外する @router.get( "/character_resources/{speaker_uuid}/{resource_name}", include_in_schema=False ) From 4b3b91653635acb4cb41c546228ae067760d234a Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 28 May 2024 00:28:31 +0900 Subject: [PATCH 04/82] FIX: `resource_url` to `return_resource_url` --- engine_manifest.json | 2 +- voicevox_engine/engine_manifest.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/engine_manifest.json b/engine_manifest.json index fd9a3b6e8..d3c17e48b 100644 --- a/engine_manifest.json +++ b/engine_manifest.json @@ -64,7 +64,7 @@ "value": true, "name": "音声ライブラリのインストール・アンインストール" }, - "resource_url":{ + "return_resource_url":{ "type":"bool", "value":true, "name": "speaker_info・singer_infoのリソースをURLで返す" diff --git a/voicevox_engine/engine_manifest.py b/voicevox_engine/engine_manifest.py index 4e3d639b3..7cce27396 100644 --- a/voicevox_engine/engine_manifest.py +++ b/voicevox_engine/engine_manifest.py @@ -31,6 +31,7 @@ class SupportedFeaturesJson(BaseModel): synthesis_morphing: FeatureSupportJson sing: FeatureSupportJson manage_library: FeatureSupportJson + return_resource_url: FeatureSupportJson class EngineManifestJson(BaseModel): @@ -93,7 +94,7 @@ class SupportedFeatures(BaseModel): manage_library: bool | None = Field( title="音声ライブラリのインストール・アンインストール" ) - resource_url: bool | None = Field( + return_resource_url: bool | None = Field( title="speaker_info・singer_infoのリソースをURLで返す" ) From 6b6da5f07a7bb6aecccd9cff699e2f95d7911ef1 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 28 May 2024 00:42:02 +0900 Subject: [PATCH 05/82] FIX: snapshot-update --- ...01\223\343\201\250\343\202\222\347\242\272\350\252\215.json" | 2 +- .../test_engine_manifest/test_get_engine_manifest_200.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" index de6e6f694..f4116cb29 100644 --- "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" +++ "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" @@ -875,7 +875,7 @@ "title": "音声ライブラリのインストール・アンインストール", "type": "boolean" }, - "resource_url": { + "return_resource_url": { "title": "speaker_info・singer_infoのリソースをURLで返す", "type": "boolean" }, diff --git a/test/e2e/single_api/engine_info/__snapshots__/test_engine_manifest/test_get_engine_manifest_200.json b/test/e2e/single_api/engine_info/__snapshots__/test_engine_manifest/test_get_engine_manifest_200.json index f8d096796..d55a925aa 100644 --- a/test/e2e/single_api/engine_info/__snapshots__/test_engine_manifest/test_get_engine_manifest_200.json +++ b/test/e2e/single_api/engine_info/__snapshots__/test_engine_manifest/test_get_engine_manifest_200.json @@ -22,7 +22,7 @@ "adjust_volume_scale": true, "interrogative_upspeak": true, "manage_library": true, - "resource_url": true, + "return_resource_url": true, "sing": true, "synthesis_morphing": true }, From 5d23138f07027b9ef915acebee5e84bad0e2cadf Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 30 May 2024 00:59:36 +0900 Subject: [PATCH 06/82] FIX: merge --- voicevox_engine/app/routers/speaker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 544919ee9..5c1c07ab7 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -1,6 +1,7 @@ """話者情報機能を提供する API Router""" import base64 +import json import traceback from os.path import basename from pathlib import Path @@ -59,8 +60,6 @@ def generate_speaker_router( """話者情報 API Router を生成する""" router = APIRouter(tags=["その他"]) - speaker_info_dir = root_dir / "speaker_info" - @router.get("/speakers") def speakers(core_version: str | None = None) -> list[Speaker]: """話者情報の一覧を取得します。""" From 918c32dffafd842f7c2e8918027a819d77832cde Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 30 May 2024 23:23:47 +0900 Subject: [PATCH 07/82] =?UTF-8?q?ENH:=20=E9=96=8B=E7=99=BA=E6=99=82?= =?UTF-8?q?=E3=81=AB`filemap.json`=E3=82=92=E4=B8=8D=E8=A6=81=E3=81=AB?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `filemap.json`の構造変更 ハッシュアルゴリズムをsha256へ リソースの管理をクラスで行う --- build_util/file_map.py | 22 ++-- speaker_info/filemap.json | 60 ----------- voicevox_engine/app/routers/speaker.py | 139 +++++++++---------------- 3 files changed, 57 insertions(+), 164 deletions(-) delete mode 100644 speaker_info/filemap.json diff --git a/build_util/file_map.py b/build_util/file_map.py index f7cf48390..5fda13a7b 100644 --- a/build_util/file_map.py +++ b/build_util/file_map.py @@ -2,7 +2,7 @@ import os from argparse import ArgumentParser from collections.abc import Generator -from hashlib import md5 +from hashlib import sha256 from pathlib import Path, PurePosixPath SPEAKER_INFO_DIR = (Path(__file__).parents[1] / "speaker_info").resolve() @@ -14,7 +14,7 @@ def to_posix_str_path(path: Path) -> str: def make_hash(file: Path) -> str: - digest = md5(file.read_bytes()).digest() + digest = sha256(file.read_bytes()).digest() return digest.hex() @@ -31,27 +31,21 @@ def walk_character_files( yield (relative, filehash) -def mapping(target: Path) -> dict[str, dict[str, str]]: +def mapping(target: Path) -> dict[str, str]: return { - character_dir.name: { - to_posix_str_path(filepath): filehash - for filepath, filehash in walk_character_files( - character_dir, ("wav", "png") - ) - } - for character_dir in target.iterdir() - if character_dir.is_dir() + to_posix_str_path(filepath): filehash + for filepath, filehash in walk_character_files(target, ("wav", "png")) } def main() -> None: parser = ArgumentParser() - parser.add_argument("--save", default=None, type=Path) + parser.add_argument("--save", type=Path) parser.add_argument("--target", default=SPEAKER_INFO_DIR, type=Path) arg = parser.parse_args() - save: Path = arg.target if arg.save is None else arg.save - save_file: Path = save if not save.is_dir() else save / DEFAULT_FILENAME target_dir: Path = arg.target + save: Path = target_dir.parent if arg.save is None else arg.save + save_file: Path = save if not save.is_dir() else save / DEFAULT_FILENAME if not target_dir.is_dir(): raise Exception() mapped = mapping(target_dir) diff --git a/speaker_info/filemap.json b/speaker_info/filemap.json deleted file mode 100644 index 2df6ab345..000000000 --- a/speaker_info/filemap.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "35b2c544-660e-401e-b503-0e14c635303a": { - "portrait.png": "bd6cf66dcc652f56892b14b423f6f37c", - "icons/8.png": "1fe576e75458c752cfeecc1e93a29886", - "portraits/8.png": "56b2cfc4a9118c40a999e151c0ac647e", - "voice_samples/8_001.wav": "62cce06e564276499df6014e7182368d", - "voice_samples/8_002.wav": "2164af6fc692d5b2117dfd845c880f81", - "voice_samples/8_003.wav": "4e81220a91745cf2ab7b632cd528ffbd" - }, - "388f246b-8c41-4ac1-8e2d-5d79f3ff56d9": { - "portrait.png": "27777cb0883c98cd9870707005bf1faf", - "icons/1.png": "becb1cc2aaf82623a13a1250a39d7393", - "icons/3.png": "9a3690c368cd9a4ecb1940ff9eb2c955", - "icons/5.png": "517ba089e0b03f8868af2ce956f7699d", - "icons/7.png": "562ad0f61ca6dd81e89a4479f97dcd9f", - "portraits/3.png": "0308b1a8e7a849e8be5ea699706f5097", - "voice_samples/1_001.wav": "a9bf75355816d858213cb116942fe499", - "voice_samples/1_002.wav": "0dc81612c1f305b6210ef325a4518e53", - "voice_samples/1_003.wav": "ba5694044d8e7e0bffa9578d22ba2ba8", - "voice_samples/3_001.wav": "0fd8a039030ea31560c84e91f955e4cd", - "voice_samples/3_002.wav": "c249264fa985fd4ab5e940c7e813db3e", - "voice_samples/3_003.wav": "6a0bc8b54543fe816f37cb286795ad07", - "voice_samples/5_001.wav": "d368ea4f7af3fba9f9f7fa862e50590f", - "voice_samples/5_002.wav": "a844eb96e25efd52dfee76d023eda0c8", - "voice_samples/5_003.wav": "4cd9a6ff5ac76ea1c267c99b2cfe925f", - "voice_samples/7_001.wav": "f214d07c3fb5332e429abb17921eecb5", - "voice_samples/7_002.wav": "46296628b586d968054cc43ad733dd4a", - "voice_samples/7_003.wav": "919e8e440b31b13fb0d2aae03061661f" - }, - "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff": { - "portrait.png": "78228648fe527ee23597b66db59a4f94", - "icons/0.png": "1f1da5f25968c638a783bf6ba9df9420", - "icons/2.png": "0b158046338f60a53e9afdb7797c5864", - "icons/4.png": "c6a8ddea789d8115372db31b4a76d2aa", - "icons/6.png": "d2c7c85a9919372ef62ad49786bd7fc4", - "portraits/0.png": "1724b3741e58978f5b9db25eb6575d9b", - "portraits/2.png": "593c75329b6531e5ae0266a708e5ebc0", - "portraits/4.png": "2b339e97d7ae0b6b215de842ff12515e", - "portraits/6.png": "6f7cd8ecb9d2d8de0ed410e05350ac72", - "voice_samples/0_001.wav": "79d52a44a8dc8548616c300e49b37a94", - "voice_samples/0_002.wav": "d31e59cf938a46d156add0da486fe5e2", - "voice_samples/0_003.wav": "73d7e1f311263ab7e1b468318cb8575a", - "voice_samples/2_001.wav": "8e917c07fe5d444eddd4b451b7dc8d83", - "voice_samples/2_002.wav": "0cf95e313bc5c3314be6a71e5021fad2", - "voice_samples/2_003.wav": "43f9f43e4607015e7ab3bd016bee5509", - "voice_samples/4_001.wav": "5333edce35b3806f1818c407cd74b66b", - "voice_samples/4_002.wav": "720ab16616e8e3e8e15535c9f6fd1f03", - "voice_samples/4_003.wav": "444dd9f019828f3773192cca7d1099df", - "voice_samples/6_001.wav": "4add3a1cf25c4b6ce6d35aaa1f760071", - "voice_samples/6_002.wav": "42d20380d8d6fc95916037e4da990aaa", - "voice_samples/6_003.wav": "af22bcd03f4959dc8f0e4ac1d04854e0" - }, - "b1a81618-b27b-40d2-b0ea-27a9ad408c4b": { - "portrait.png": "381ba07cbcad95f1c99109f5f6d096a8", - "icons/9.png": "1e3d0a6e88264b1b99839aeb7c7cbe1f", - "voice_samples/9_001.wav": "e8626456ba706690676639e61ad43b73", - "voice_samples/9_002.wav": "93e3d5c44bbad3ddfc43953f5c751670", - "voice_samples/9_003.wav": "765ff637563e64182ace61fb5ca7daf4" - } -} diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 5c1c07ab7..18facc4a3 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -3,12 +3,12 @@ import base64 import json import traceback -from os.path import basename +from hashlib import sha256 from pathlib import Path from typing import Annotated, Literal -from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response -from fastapi.responses import FileResponse +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi.responses import FileResponse, Response from pydantic import parse_obj_as from voicevox_engine.core.core_initializer import CoreManager @@ -16,40 +16,41 @@ from voicevox_engine.metas.MetasStore import MetasStore, filter_speakers_and_styles from voicevox_engine.model import Speaker, SpeakerInfo +RESOURCE_ENDPOINT = "resources" + async def get_character_resource_baseurl(request: Request) -> str: - return f"{request.url.scheme}://{request.url.netloc}/character_resources" + return f"{request.url.scheme}://{request.url.netloc}/{RESOURCE_ENDPOINT}" def b64encode_str(s: bytes) -> str: return base64.b64encode(s).decode("utf-8") -def read_mapfile(speaker_info_dir: Path) -> dict[str, dict[str, str]]: - filemap_path = speaker_info_dir / "filemap.json" - with filemap_path.open(mode="rb") as f: - filemap: dict[str, dict[str, str]] = json.load(f) - return filemap - - -def gen_file_name(filehash: str, filename: str) -> str: - return f"{filehash}_{filename}" - - -def file_url(base_url: str, speaker_id: str, filehash: str, filename: str) -> str: - return f"{base_url}/{speaker_id}/" + gen_file_name(filehash, filename) +class SpeakerResourceManager: + def __init__(self, speaker_info_dir: Path, is_development: bool) -> None: + try: + with (speaker_info_dir.parent / "filemap.json").open(mode="rb") as f: + data: dict[str, str] = json.load(f) + self.filemap = {speaker_info_dir / k: v for k, v in data.items()} + except FileNotFoundError as e: + if is_development: + self.filemap = { + i: sha256(i.read_bytes()).digest().hex() + for i in speaker_info_dir.glob("**/*") + if i.is_file() + } + else: + raise e + self.hashmap = {v: k for k, v in self.filemap.items()} + def resource_str(self, resource_path: Path, base_url: str, return_url: bool) -> str: + if not return_url: + return b64encode_str(resource_path.read_bytes()) + return f"{base_url}/{self.filemap[resource_path]}" -def gen_hash_to_resource_id_mapping( - filemap: dict[str, dict[str, str]] -) -> dict[str, dict[str, str]]: - return { - speaker_uuid: { - f"{filehash}_{basename(filepath)}": filepath - for filepath, filehash in ph.items() - } - for speaker_uuid, ph in filemap.items() - } + def resource_path(self, filehash: str) -> Path: + return self.hashmap[filehash] def generate_speaker_router( @@ -85,7 +86,7 @@ def speaker_info( resource_url=resource_url, ) - mapfile = read_mapfile(speaker_info_dir) + manager = SpeakerResourceManager(speaker_info_dir, True) # FIXME: この関数をどこかに切り出す def _speaker_info( @@ -138,17 +139,8 @@ def _speaker_info( policy = policy_path.read_text("utf-8") # speaker portrait - if resource_url: - speaker_map = mapfile[speaker_uuid] - portrait = file_url( - self_url, - speaker_uuid, - speaker_map["portrait.png"], - "portrait.png", - ) - else: - portrait_path = speaker_path / "portrait.png" - portrait = b64encode_str(portrait_path.read_bytes()) + portrait_path = speaker_path / "portrait.png" + portrait = manager.resource_str(portrait_path, self_url, resource_url) # スタイル情報を取得する style_infos = [] @@ -156,48 +148,26 @@ def _speaker_info( id = style.id # style icon - style_icon_name = f"{id}.png" + style_icon_path = speaker_path / "icons" / f"{id}.png" + icon = manager.resource_str(style_icon_path, self_url, resource_url) # style portrait - style_portrait_name = f"{id}.png" + style_portrait_path = speaker_path / "portraits" / f"{id}.png" style_portrait = None + if style_portrait_path.exists(): + style_portrait = manager.resource_str( + style_portrait_path, self_url, resource_url + ) # voice samples - voice_samples_names = [ - "{}_{}.wav".format(id, str(j + 1).zfill(3)) for j in range(3) - ] - if resource_url: - icon_hash = speaker_map[f"icons/{style_icon_name}"] - icon = file_url(self_url, speaker_uuid, icon_hash, style_icon_name) - style_portrait_hash = speaker_map.get( - f"portraits/{style_portrait_name}" - ) - if style_portrait_hash is not None: - style_portrait = file_url( - self_url, speaker_uuid, style_portrait_hash, style_icon_name - ) - voice_samples_hashes = { - speaker_map[f"voice_samples/{name}"]: name - for name in voice_samples_names - } - voice_samples = [ - file_url(self_url, speaker_uuid, k, v) - for k, v in voice_samples_hashes.items() - ] - else: - style_icon_path = speaker_path / "icons" / style_icon_name - icon = b64encode_str(style_icon_path.read_bytes()) - style_portrait_path = ( - speaker_path / "portraits" / style_portrait_name + voice_samples: list[str] = [] + for j in range(3): + num = str(j + 1).zfill(3) + voice_path = speaker_path / "voice_samples" / f"{id}_{num}.wav" + voice_samples.append( + manager.resource_str(voice_path, self_url, resource_url) ) - if style_portrait_path.exists(): - style_portrait = b64encode_str(style_portrait_path.read_bytes()) - voice_samples = [ - b64encode_str( - (speaker_path / "voice_samples" / name).read_bytes() - ) - for name in voice_samples_names - ] + style_infos.append( { "id": id, @@ -241,25 +211,14 @@ def singer_info( resource_url=resource_url, ) - resource_id_mapping = gen_hash_to_resource_id_mapping(mapfile) - # リソースはAPIとしてアクセスするものではないことを表明するためOpenAPIスキーマーから除外する - @router.get( - "/character_resources/{speaker_uuid}/{resource_name}", include_in_schema=False - ) - async def static( - request: Request, speaker_uuid: str, resource_name: str - ) -> Response: - try: - resource = resource_id_mapping[speaker_uuid][resource_name] - except KeyError: - raise HTTPException(status_code=404) + @router.get(f"/{RESOURCE_ENDPOINT}/{{resource_name}}", include_in_schema=False) + async def resources(request: Request, resource_name: str) -> Response: headers = { "Cache-Control": "max-age=2592000, immutable, stale-while-revalidate" } - response = FileResponse( - speaker_info_dir / speaker_uuid / resource, headers=headers - ) + resource_path = manager.resource_path(resource_name) + response = FileResponse(resource_path, headers=headers) return response @router.post("/initialize_speaker", status_code=204) From 18c1bc10804895d9d7f23d3ff9da9e2a154c5ac4 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:58:02 +0900 Subject: [PATCH 08/82] Update voicevox_engine/engine_manifest.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `EngineManifest`のテキスト変更 Co-authored-by: Hiroshiba --- voicevox_engine/engine_manifest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicevox_engine/engine_manifest.py b/voicevox_engine/engine_manifest.py index c6e3811c9..ec4f5962d 100644 --- a/voicevox_engine/engine_manifest.py +++ b/voicevox_engine/engine_manifest.py @@ -96,7 +96,7 @@ class SupportedFeatures(BaseModel): title="音声ライブラリのインストール・アンインストール" ) return_resource_url: bool | None = Field( - title="speaker_info・singer_infoのリソースをURLで返す" + title="speaker_info・singer_infoのリソースをURLで返送" ) From 613fa04fbbc1ceb07109befce6d9c8c876aaccf6 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:59:49 +0900 Subject: [PATCH 09/82] =?UTF-8?q?FIX:=20`engine=5Fmanifest.json`=E3=81=AE?= =?UTF-8?q?=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- engine_manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine_manifest.json b/engine_manifest.json index d3c17e48b..000edb6dc 100644 --- a/engine_manifest.json +++ b/engine_manifest.json @@ -67,7 +67,7 @@ "return_resource_url":{ "type":"bool", "value":true, - "name": "speaker_info・singer_infoのリソースをURLで返す" + "name": "speaker_info・singer_infoのリソースをURLで返送" } } } From df46c52be1e386100ee1a1c90b0def4ea21141f5 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 3 Jun 2024 23:37:27 +0900 Subject: [PATCH 10/82] =?UTF-8?q?MNT:=20=E9=96=A2=E6=95=B0=E5=90=8D?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 6a6626f73..e6b66c17b 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -17,7 +17,7 @@ RESOURCE_ENDPOINT = "resources" -async def get_character_resource_baseurl(request: Request) -> str: +async def get_resource_baseurl(request: Request) -> str: return f"{request.url.scheme}://{request.url.netloc}/{RESOURCE_ENDPOINT}" @@ -68,7 +68,7 @@ def speakers(core_version: str | None = None) -> list[Speaker]: @router.get("/speaker_info") def speaker_info( - self_url: Annotated[str, Depends(get_character_resource_baseurl)], + self_url: Annotated[str, Depends(get_resource_baseurl)], speaker_uuid: str, resource_url: bool = False, core_version: str | None = None, @@ -193,7 +193,7 @@ def singers(core_version: str | None = None) -> list[Speaker]: @router.get("/singer_info") def singer_info( - self_url: Annotated[str, Depends(get_character_resource_baseurl)], + self_url: Annotated[str, Depends(get_resource_baseurl)], speaker_uuid: str, resource_url: bool = False, core_version: str | None = None, From 6bb6a03c3334f6899111bc63284a4ba96ee09b55 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 3 Jun 2024 23:45:28 +0900 Subject: [PATCH 11/82] FIX: `resource_url` to `resource_format` --- voicevox_engine/app/routers/speaker.py | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index e6b66c17b..29922c556 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -42,8 +42,13 @@ def __init__(self, speaker_info_dir: Path, is_development: bool) -> None: raise e self.hashmap = {v: k for k, v in self.filemap.items()} - def resource_str(self, resource_path: Path, base_url: str, return_url: bool) -> str: - if not return_url: + def resource_str( + self, + resource_path: Path, + base_url: str, + resource_format: Literal["base64", "url"], + ) -> str: + if resource_format == "base64": return b64encode_str(resource_path.read_bytes()) return f"{base_url}/{self.filemap[resource_path]}" @@ -70,7 +75,7 @@ def speakers(core_version: str | None = None) -> list[Speaker]: def speaker_info( self_url: Annotated[str, Depends(get_resource_baseurl)], speaker_uuid: str, - resource_url: bool = False, + resource_format: Literal["base64", "url"] = "base64", core_version: str | None = None, ) -> SpeakerInfo: """ @@ -82,7 +87,7 @@ def speaker_info( speaker_or_singer="speaker", core_version=core_version, self_url=self_url, - resource_url=resource_url, + resource_format=resource_format, ) manager = SpeakerResourceManager(speaker_info_dir, True) @@ -93,7 +98,7 @@ def _speaker_info( speaker_or_singer: Literal["speaker", "singer"], core_version: str | None, self_url: str, - resource_url: bool, + resource_format: Literal["base64", "url"], ) -> SpeakerInfo: # エンジンに含まれる話者メタ情報は、次のディレクトリ構造に従わなければならない: # {root_dir}/ @@ -139,7 +144,7 @@ def _speaker_info( # speaker portrait portrait_path = speaker_path / "portrait.png" - portrait = manager.resource_str(portrait_path, self_url, resource_url) + portrait = manager.resource_str(portrait_path, self_url, resource_format) # スタイル情報を取得する style_infos = [] @@ -148,14 +153,14 @@ def _speaker_info( # style icon style_icon_path = speaker_path / "icons" / f"{id}.png" - icon = manager.resource_str(style_icon_path, self_url, resource_url) + icon = manager.resource_str(style_icon_path, self_url, resource_format) # style portrait style_portrait_path = speaker_path / "portraits" / f"{id}.png" style_portrait = None if style_portrait_path.exists(): style_portrait = manager.resource_str( - style_portrait_path, self_url, resource_url + style_portrait_path, self_url, resource_format ) # voice samples @@ -164,7 +169,7 @@ def _speaker_info( num = str(j + 1).zfill(3) voice_path = speaker_path / "voice_samples" / f"{id}_{num}.wav" voice_samples.append( - manager.resource_str(voice_path, self_url, resource_url) + manager.resource_str(voice_path, self_url, resource_format) ) style_infos.append( @@ -195,7 +200,7 @@ def singers(core_version: str | None = None) -> list[Speaker]: def singer_info( self_url: Annotated[str, Depends(get_resource_baseurl)], speaker_uuid: str, - resource_url: bool = False, + resource_format: Literal["base64", "url"] = "base64", core_version: str | None = None, ) -> SpeakerInfo: """ @@ -207,7 +212,7 @@ def singer_info( speaker_or_singer="singer", core_version=core_version, self_url=self_url, - resource_url=resource_url, + resource_format=resource_format, ) # リソースはAPIとしてアクセスするものではないことを表明するためOpenAPIスキーマーから除外する From a40a7d0cd58c75c73ce4aa34397206a0c1465c35 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 3 Jun 2024 23:48:17 +0900 Subject: [PATCH 12/82] TST: update snapshot --- ...\343\202\222\347\242\272\350\252\215.json" | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" index 78374b7ba..0c4ac8283 100644 --- "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" +++ "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" @@ -876,7 +876,7 @@ "type": "boolean" }, "return_resource_url": { - "title": "speaker_info・singer_infoのリソースをURLで返す", + "title": "speaker_info・singer_infoのリソースをURLで返送", "type": "boolean" }, "sing": { @@ -2456,12 +2456,16 @@ }, { "in": "query", - "name": "resource_url", + "name": "resource_format", "required": false, "schema": { - "default": false, - "title": "Resource Url", - "type": "boolean" + "default": "base64", + "enum": [ + "base64", + "url" + ], + "title": "Resource Format", + "type": "string" } }, { @@ -2565,12 +2569,16 @@ }, { "in": "query", - "name": "resource_url", + "name": "resource_format", "required": false, "schema": { - "default": false, - "title": "Resource Url", - "type": "boolean" + "default": "base64", + "enum": [ + "base64", + "url" + ], + "title": "Resource Format", + "type": "string" } }, { From f9992349cdbcd45124adb6121e180af8194b3b85 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 4 Jun 2024 00:01:17 +0900 Subject: [PATCH 13/82] =?UTF-8?q?FIX:=20`SpeakerResourceManager`=E5=91=A8?= =?UTF-8?q?=E3=82=8A=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 29922c556..ad12753bb 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -25,22 +25,22 @@ def b64encode_str(s: bytes) -> str: return base64.b64encode(s).decode("utf-8") -class SpeakerResourceManager: +class ResourceManager: def __init__(self, speaker_info_dir: Path, is_development: bool) -> None: - try: - with (speaker_info_dir.parent / "filemap.json").open(mode="rb") as f: - data: dict[str, str] = json.load(f) - self.filemap = {speaker_info_dir / k: v for k, v in data.items()} - except FileNotFoundError as e: + filemap_json = speaker_info_dir.parent / "filemap.json" + if filemap_json.exists(): + data: dict[str, str] = json.load(filemap_json.read_bytes()) + self._file_to_hash = {speaker_info_dir / k: v for k, v in data.items()} + else: if is_development: - self.filemap = { + self._file_to_hash = { i: sha256(i.read_bytes()).digest().hex() for i in speaker_info_dir.glob("**/*") if i.is_file() } else: - raise e - self.hashmap = {v: k for k, v in self.filemap.items()} + raise Exception(f"{filemap_json}が見つかりません") + self._hash_to_file = {v: k for k, v in self._file_to_hash.items()} def resource_str( self, @@ -50,10 +50,10 @@ def resource_str( ) -> str: if resource_format == "base64": return b64encode_str(resource_path.read_bytes()) - return f"{base_url}/{self.filemap[resource_path]}" + return f"{base_url}/{self._file_to_hash[resource_path]}" def resource_path(self, filehash: str) -> Path: - return self.hashmap[filehash] + return self._hash_to_file[filehash] def generate_speaker_router( @@ -90,7 +90,7 @@ def speaker_info( resource_format=resource_format, ) - manager = SpeakerResourceManager(speaker_info_dir, True) + manager = ResourceManager(speaker_info_dir, True) # FIXME: この関数をどこかに切り出す def _speaker_info( From 8835a96cdd0e2723743278a598c71b05841d273e Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 4 Jun 2024 01:02:21 +0900 Subject: [PATCH 14/82] =?UTF-8?q?MNT:=20`ResourceManager`=E5=88=86?= =?UTF-8?q?=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/application.py | 7 ++- voicevox_engine/app/routers/speaker.py | 71 ++++++-------------------- voicevox_engine/resource_manager.py | 40 +++++++++++++++ 3 files changed, 63 insertions(+), 55 deletions(-) create mode 100644 voicevox_engine/resource_manager.py diff --git a/voicevox_engine/app/application.py b/voicevox_engine/app/application.py index 8c9c83b17..887e4a3b5 100644 --- a/voicevox_engine/app/application.py +++ b/voicevox_engine/app/application.py @@ -24,6 +24,7 @@ from voicevox_engine.library.library_manager import LibraryManager from voicevox_engine.metas.MetasStore import MetasStore from voicevox_engine.preset.preset_manager import PresetManager +from voicevox_engine.resource_manager import ResourceManager from voicevox_engine.setting.model import CorsPolicyMode from voicevox_engine.setting.setting_manager import SettingHandler from voicevox_engine.tts_pipeline.tts_engine import TTSEngineManager @@ -69,8 +70,12 @@ def generate_app( ) app.include_router(generate_morphing_router(tts_engines, core_manager, metas_store)) app.include_router(generate_preset_router(preset_manager)) + + resource_manager = ResourceManager(speaker_info_dir, True) app.include_router( - generate_speaker_router(core_manager, metas_store, speaker_info_dir) + generate_speaker_router( + core_manager, resource_manager, metas_store, speaker_info_dir + ) ) if engine_manifest.supported_features.manage_library: app.include_router(generate_library_router(library_manager)) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index ad12753bb..cccc1751a 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -1,8 +1,5 @@ """話者情報機能を提供する API Router""" -import base64 -import json -from hashlib import sha256 from pathlib import Path from typing import Annotated, Literal @@ -13,6 +10,7 @@ from voicevox_engine.core.core_initializer import CoreManager from voicevox_engine.metas.Metas import Speaker, SpeakerInfo from voicevox_engine.metas.MetasStore import MetasStore, filter_speakers_and_styles +from voicevox_engine.resource_manager import ResourceManager RESOURCE_ENDPOINT = "resources" @@ -21,43 +19,9 @@ async def get_resource_baseurl(request: Request) -> str: return f"{request.url.scheme}://{request.url.netloc}/{RESOURCE_ENDPOINT}" -def b64encode_str(s: bytes) -> str: - return base64.b64encode(s).decode("utf-8") - - -class ResourceManager: - def __init__(self, speaker_info_dir: Path, is_development: bool) -> None: - filemap_json = speaker_info_dir.parent / "filemap.json" - if filemap_json.exists(): - data: dict[str, str] = json.load(filemap_json.read_bytes()) - self._file_to_hash = {speaker_info_dir / k: v for k, v in data.items()} - else: - if is_development: - self._file_to_hash = { - i: sha256(i.read_bytes()).digest().hex() - for i in speaker_info_dir.glob("**/*") - if i.is_file() - } - else: - raise Exception(f"{filemap_json}が見つかりません") - self._hash_to_file = {v: k for k, v in self._file_to_hash.items()} - - def resource_str( - self, - resource_path: Path, - base_url: str, - resource_format: Literal["base64", "url"], - ) -> str: - if resource_format == "base64": - return b64encode_str(resource_path.read_bytes()) - return f"{base_url}/{self._file_to_hash[resource_path]}" - - def resource_path(self, filehash: str) -> Path: - return self._hash_to_file[filehash] - - def generate_speaker_router( core_manager: CoreManager, + resource_manager: ResourceManager, metas_store: MetasStore, speaker_info_dir: Path, ) -> APIRouter: @@ -73,7 +37,7 @@ def speakers(core_version: str | None = None) -> list[Speaker]: @router.get("/speaker_info") def speaker_info( - self_url: Annotated[str, Depends(get_resource_baseurl)], + resource_baseurl: Annotated[str, Depends(get_resource_baseurl)], speaker_uuid: str, resource_format: Literal["base64", "url"] = "base64", core_version: str | None = None, @@ -86,18 +50,16 @@ def speaker_info( speaker_uuid=speaker_uuid, speaker_or_singer="speaker", core_version=core_version, - self_url=self_url, + resource_baseurl=resource_baseurl, resource_format=resource_format, ) - manager = ResourceManager(speaker_info_dir, True) - # FIXME: この関数をどこかに切り出す def _speaker_info( speaker_uuid: str, speaker_or_singer: Literal["speaker", "singer"], core_version: str | None, - self_url: str, + resource_baseurl: str, resource_format: Literal["base64", "url"], ) -> SpeakerInfo: # エンジンに含まれる話者メタ情報は、次のディレクトリ構造に従わなければならない: @@ -142,9 +104,14 @@ def _speaker_info( policy_path = speaker_path / "policy.md" policy = policy_path.read_text("utf-8") + def _resource_str(path: Path) -> str: + return resource_manager.resource_str( + path, resource_baseurl, resource_format + ) + # speaker portrait portrait_path = speaker_path / "portrait.png" - portrait = manager.resource_str(portrait_path, self_url, resource_format) + portrait = _resource_str(portrait_path) # スタイル情報を取得する style_infos = [] @@ -153,24 +120,20 @@ def _speaker_info( # style icon style_icon_path = speaker_path / "icons" / f"{id}.png" - icon = manager.resource_str(style_icon_path, self_url, resource_format) + icon = _resource_str(style_icon_path) # style portrait style_portrait_path = speaker_path / "portraits" / f"{id}.png" style_portrait = None if style_portrait_path.exists(): - style_portrait = manager.resource_str( - style_portrait_path, self_url, resource_format - ) + style_portrait = _resource_str(style_portrait_path) # voice samples voice_samples: list[str] = [] for j in range(3): num = str(j + 1).zfill(3) voice_path = speaker_path / "voice_samples" / f"{id}_{num}.wav" - voice_samples.append( - manager.resource_str(voice_path, self_url, resource_format) - ) + voice_samples.append(_resource_str(voice_path)) style_infos.append( { @@ -198,7 +161,7 @@ def singers(core_version: str | None = None) -> list[Speaker]: @router.get("/singer_info") def singer_info( - self_url: Annotated[str, Depends(get_resource_baseurl)], + resource_baseurl: Annotated[str, Depends(get_resource_baseurl)], speaker_uuid: str, resource_format: Literal["base64", "url"] = "base64", core_version: str | None = None, @@ -211,7 +174,7 @@ def singer_info( speaker_uuid=speaker_uuid, speaker_or_singer="singer", core_version=core_version, - self_url=self_url, + resource_baseurl=resource_baseurl, resource_format=resource_format, ) @@ -221,7 +184,7 @@ async def resources(request: Request, resource_name: str) -> Response: headers = { "Cache-Control": "max-age=2592000, immutable, stale-while-revalidate" } - resource_path = manager.resource_path(resource_name) + resource_path = resource_manager.resource_path(resource_name) response = FileResponse(resource_path, headers=headers) return response diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py new file mode 100644 index 000000000..d9b1d7c48 --- /dev/null +++ b/voicevox_engine/resource_manager.py @@ -0,0 +1,40 @@ +import base64 +import json +from hashlib import sha256 +from pathlib import Path +from typing import Literal + + +def b64encode_str(s: bytes) -> str: + return base64.b64encode(s).decode("utf-8") + + +class ResourceManager: + def __init__(self, resource_dir: Path, is_development: bool) -> None: + filemap_json = resource_dir.parent / "filemap.json" + if filemap_json.exists(): + data: dict[str, str] = json.loads(filemap_json.read_bytes()) + self._file_to_hash = {resource_dir / k: v for k, v in data.items()} + else: + if is_development: + self._file_to_hash = { + i: sha256(i.read_bytes()).digest().hex() + for i in resource_dir.glob("**/*") + if i.is_file() + } + else: + raise Exception(f"{filemap_json}が見つかりません") + self._hash_to_file = {v: k for k, v in self._file_to_hash.items()} + + def resource_str( + self, + resource_path: Path, + base_url: str, + resource_format: Literal["base64", "url"], + ) -> str: + if resource_format == "base64": + return b64encode_str(resource_path.read_bytes()) + return f"{base_url}/{self._file_to_hash[resource_path]}" + + def resource_path(self, filehash: str) -> Path: + return self._hash_to_file[filehash] From b96a5f9d3b76f965df68c8a1a16f9879cfd0a1f3 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:46:26 +0900 Subject: [PATCH 15/82] =?UTF-8?q?ENH:=20=E3=82=88=E3=82=8A=E6=B1=8E?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E3=81=AA=E5=AE=9F=E8=A3=85=E3=81=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/application.py | 3 ++- voicevox_engine/resource_manager.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/voicevox_engine/app/application.py b/voicevox_engine/app/application.py index 887e4a3b5..b0fd14768 100644 --- a/voicevox_engine/app/application.py +++ b/voicevox_engine/app/application.py @@ -71,7 +71,8 @@ def generate_app( app.include_router(generate_morphing_router(tts_engines, core_manager, metas_store)) app.include_router(generate_preset_router(preset_manager)) - resource_manager = ResourceManager(speaker_info_dir, True) + resource_manager = ResourceManager(True) + resource_manager.register_dir(speaker_info_dir) app.include_router( generate_speaker_router( core_manager, resource_manager, metas_store, speaker_info_dir diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index d9b1d7c48..9d0e8f8da 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -10,21 +10,26 @@ def b64encode_str(s: bytes) -> str: class ResourceManager: - def __init__(self, resource_dir: Path, is_development: bool) -> None: + def __init__(self, is_development: bool) -> None: + self._is_development = is_development + self._file_to_hash: dict[Path, str] = {} + self._hash_to_file: dict[str, Path] = {} + + def register_dir(self, resource_dir: Path) -> None: filemap_json = resource_dir.parent / "filemap.json" if filemap_json.exists(): data: dict[str, str] = json.loads(filemap_json.read_bytes()) - self._file_to_hash = {resource_dir / k: v for k, v in data.items()} + self._file_to_hash |= {resource_dir / k: v for k, v in data.items()} else: - if is_development: - self._file_to_hash = { + if self._is_development: + self._file_to_hash |= { i: sha256(i.read_bytes()).digest().hex() for i in resource_dir.glob("**/*") if i.is_file() } else: raise Exception(f"{filemap_json}が見つかりません") - self._hash_to_file = {v: k for k, v in self._file_to_hash.items()} + self._hash_to_file |= {v: k for k, v in self._file_to_hash.items()} def resource_str( self, From 1772a43fea18ea9280ede3a7675e59d934a81902 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:01:09 +0900 Subject: [PATCH 16/82] =?UTF-8?q?MNT:=20`file=5Fmap.py`=E3=81=AE=E6=94=B9?= =?UTF-8?q?=E8=89=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_util/file_map.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/build_util/file_map.py b/build_util/file_map.py index 5fda13a7b..f3fc92b14 100644 --- a/build_util/file_map.py +++ b/build_util/file_map.py @@ -1,3 +1,7 @@ +""" +'ResourceManager'が参照するファイルを生成 +""" + import json import os from argparse import ArgumentParser @@ -5,8 +9,8 @@ from hashlib import sha256 from pathlib import Path, PurePosixPath -SPEAKER_INFO_DIR = (Path(__file__).parents[1] / "speaker_info").resolve() DEFAULT_FILENAME = "filemap.json" +DEFAULT_TARGET_SUFFIX = ["png", "wav"] def to_posix_str_path(path: Path) -> str: @@ -18,7 +22,7 @@ def make_hash(file: Path) -> str: return digest.hex() -def walk_character_files( +def walk_target_dir( character_dir: Path, suffix: tuple[str, ...] ) -> Generator[tuple[Path, str], None, None]: for root, _, files in os.walk(character_dir): @@ -31,26 +35,34 @@ def walk_character_files( yield (relative, filehash) -def mapping(target: Path) -> dict[str, str]: +def generate_path_to_hash_dict( + target_dir: Path, target_suffix: list[str] +) -> dict[str, str]: + suffix = tuple(target_suffix) return { to_posix_str_path(filepath): filehash - for filepath, filehash in walk_character_files(target, ("wav", "png")) + for filepath, filehash in walk_target_dir(target_dir, suffix) } def main() -> None: parser = ArgumentParser() - parser.add_argument("--save", type=Path) - parser.add_argument("--target", default=SPEAKER_INFO_DIR, type=Path) + parser.add_argument( + "--target_dir", type=Path, required=True, help="filemapを作成するディレクトリ" + ) + parser.add_argument( + "--target_suffix", + nargs="+", + default=DEFAULT_TARGET_SUFFIX, + help=f"filemapに登録するファイルの拡張子\nデフォルトは{', '.join(DEFAULT_TARGET_SUFFIX)}", + ) arg = parser.parse_args() - target_dir: Path = arg.target - save: Path = target_dir.parent if arg.save is None else arg.save - save_file: Path = save if not save.is_dir() else save / DEFAULT_FILENAME + target_dir: Path = arg.target_dir if not target_dir.is_dir(): - raise Exception() - mapped = mapping(target_dir) - with save_file.open(mode="wt", encoding="utf-8") as f: - json.dump(mapped, f, ensure_ascii=False) + raise Exception(f"{target_dir}はディレクトリではありません") + save_path = target_dir.parent / DEFAULT_FILENAME + path_to_hash = generate_path_to_hash_dict(target_dir, arg.target_suffix) + save_path.write_text(json.dumps(path_to_hash, ensure_ascii=False), encoding="utf-8") if __name__ == "__main__": From 2fc05c655e1da22767e575f053230a744a698a00 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:24:31 +0900 Subject: [PATCH 17/82] MNT: `_file_to_hash` to `_path_to_hash` --- voicevox_engine/resource_manager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 9d0e8f8da..dd2fe84cb 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -12,24 +12,24 @@ def b64encode_str(s: bytes) -> str: class ResourceManager: def __init__(self, is_development: bool) -> None: self._is_development = is_development - self._file_to_hash: dict[Path, str] = {} - self._hash_to_file: dict[str, Path] = {} + self._path_to_hash: dict[Path, str] = {} + self._hash_to_path: dict[str, Path] = {} def register_dir(self, resource_dir: Path) -> None: filemap_json = resource_dir.parent / "filemap.json" if filemap_json.exists(): data: dict[str, str] = json.loads(filemap_json.read_bytes()) - self._file_to_hash |= {resource_dir / k: v for k, v in data.items()} + self._path_to_hash |= {resource_dir / k: v for k, v in data.items()} else: if self._is_development: - self._file_to_hash |= { + self._path_to_hash |= { i: sha256(i.read_bytes()).digest().hex() for i in resource_dir.glob("**/*") if i.is_file() } else: raise Exception(f"{filemap_json}が見つかりません") - self._hash_to_file |= {v: k for k, v in self._file_to_hash.items()} + self._hash_to_path |= {v: k for k, v in self._path_to_hash.items()} def resource_str( self, @@ -39,7 +39,7 @@ def resource_str( ) -> str: if resource_format == "base64": return b64encode_str(resource_path.read_bytes()) - return f"{base_url}/{self._file_to_hash[resource_path]}" + return f"{base_url}/{self._path_to_hash[resource_path]}" def resource_path(self, filehash: str) -> Path: - return self._hash_to_file[filehash] + return self._hash_to_path[filehash] From dc72d77658051e11bb5f4b2fa6f0c851bb7cef4c Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:06:26 +0900 Subject: [PATCH 18/82] =?UTF-8?q?FIX:=20`resources()`=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 17 ++++++++++++----- voicevox_engine/resource_manager.py | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index cccc1751a..1738c1555 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -180,12 +180,19 @@ def singer_info( # リソースはAPIとしてアクセスするものではないことを表明するためOpenAPIスキーマーから除外する @router.get(f"/{RESOURCE_ENDPOINT}/{{resource_name}}", include_in_schema=False) - async def resources(request: Request, resource_name: str) -> Response: - headers = { - "Cache-Control": "max-age=2592000, immutable, stale-while-revalidate" - } + def resources(resource_name: str) -> Response: + """ + ResourceManagerから発行されたURLへのアクセスに対応する + """ resource_path = resource_manager.resource_path(resource_name) - response = FileResponse(resource_path, headers=headers) + if resource_path is None or not resource_path.exists(): + raise HTTPException(status_code=404) + response = FileResponse( + resource_path, + headers={ + "Cache-Control": "max-age=2592000, immutable, stale-while-revalidate=2592000" + }, + ) return response return router diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index dd2fe84cb..bc9632d86 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -41,5 +41,5 @@ def resource_str( return b64encode_str(resource_path.read_bytes()) return f"{base_url}/{self._path_to_hash[resource_path]}" - def resource_path(self, filehash: str) -> Path: - return self._hash_to_path[filehash] + def resource_path(self, filehash: str) -> Path | None: + return self._hash_to_path.get(filehash) From f2b4c63d152fbddcb3b2f9dfa0181ddb78136f25 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:29:29 +0900 Subject: [PATCH 19/82] =?UTF-8?q?FIX:=20`filemap.json`=E3=81=AE=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E5=A0=B4=E6=89=80=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_util/file_map.py | 8 ++++---- voicevox_engine/resource_manager.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build_util/file_map.py b/build_util/file_map.py index f3fc92b14..c3554c0f7 100644 --- a/build_util/file_map.py +++ b/build_util/file_map.py @@ -23,15 +23,15 @@ def make_hash(file: Path) -> str: def walk_target_dir( - character_dir: Path, suffix: tuple[str, ...] + target_dir: Path, suffix: tuple[str, ...] ) -> Generator[tuple[Path, str], None, None]: - for root, _, files in os.walk(character_dir): + for root, _, files in os.walk(target_dir): for file in files: filepath = Path(root, file) if not filepath.suffix.endswith(suffix): continue filehash = make_hash(filepath) - relative = filepath.relative_to(character_dir) + relative = filepath.relative_to(target_dir) yield (relative, filehash) @@ -60,7 +60,7 @@ def main() -> None: target_dir: Path = arg.target_dir if not target_dir.is_dir(): raise Exception(f"{target_dir}はディレクトリではありません") - save_path = target_dir.parent / DEFAULT_FILENAME + save_path = target_dir / DEFAULT_FILENAME path_to_hash = generate_path_to_hash_dict(target_dir, arg.target_suffix) save_path.write_text(json.dumps(path_to_hash, ensure_ascii=False), encoding="utf-8") diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index bc9632d86..1dd1adb9b 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -16,7 +16,7 @@ def __init__(self, is_development: bool) -> None: self._hash_to_path: dict[str, Path] = {} def register_dir(self, resource_dir: Path) -> None: - filemap_json = resource_dir.parent / "filemap.json" + filemap_json = resource_dir / "filemap.json" if filemap_json.exists(): data: dict[str, str] = json.loads(filemap_json.read_bytes()) self._path_to_hash |= {resource_dir / k: v for k, v in data.items()} From 8f331c8c67585a6222501ab32574bc056fe6e8b1 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 6 Jun 2024 01:31:05 +0900 Subject: [PATCH 20/82] =?UTF-8?q?ENH:=20=E3=83=AC=E3=82=B9=E3=83=9D?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=81=AE=E6=A4=9C=E8=A8=BC=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 41 ++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 1738c1555..09c886bb1 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -1,9 +1,10 @@ """話者情報機能を提供する API Router""" +from email.utils import parsedate from pathlib import Path from typing import Annotated, Literal -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, Header, HTTPException, Request from fastapi.responses import FileResponse, Response from pydantic import parse_obj_as @@ -180,7 +181,11 @@ def singer_info( # リソースはAPIとしてアクセスするものではないことを表明するためOpenAPIスキーマーから除外する @router.get(f"/{RESOURCE_ENDPOINT}/{{resource_name}}", include_in_schema=False) - def resources(resource_name: str) -> Response: + def resources( + resource_name: str, + if_none_match: Annotated[str | None, Header()] = None, + if_modified_since: Annotated[str | None, Header()] = None, + ) -> Response: """ ResourceManagerから発行されたURLへのアクセスに対応する """ @@ -192,7 +197,39 @@ def resources(resource_name: str) -> Response: headers={ "Cache-Control": "max-age=2592000, immutable, stale-while-revalidate=2592000" }, + stat_result=resource_path.stat(), ) + res_headers = response.headers + # 304レスポンスの作成 + modified_headers = { + k: res_headers.get(k) + for k in [ + "Cache-Control", + "Content-Location", + "Date", + "ETag", + "Expires", + "Vary", + ] + } + modified_response = Response( + status_code=304, + headers={k: v for k, v in modified_headers.items() if v is not None}, + ) + # ETagとLast-Modifiedの検証 + if if_none_match is not None: + etag = res_headers["ETag"] + if etag in [tag.strip(" W/") for tag in if_none_match.split(",")]: + return modified_response + elif if_modified_since is not None: + _if_modified_since = parsedate(if_modified_since) + last_modified = parsedate(res_headers["Last-Modified"]) + if ( + _if_modified_since is not None + and last_modified is not None + and _if_modified_since >= last_modified + ): + return modified_response return response return router From 02ef06daca7026b1b7ee1442523cebd75506c4b3 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:15:13 +0900 Subject: [PATCH 21/82] =?UTF-8?q?FIX:=20=E5=86=97=E9=95=B7=E3=81=AA?= =?UTF-8?q?=E9=83=A8=E5=88=86=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 09c886bb1..7f202a9a5 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -201,20 +201,20 @@ def resources( ) res_headers = response.headers # 304レスポンスの作成 - modified_headers = { - k: res_headers.get(k) - for k in [ - "Cache-Control", - "Content-Location", - "Date", - "ETag", - "Expires", - "Vary", - ] - } modified_response = Response( status_code=304, - headers={k: v for k, v in modified_headers.items() if v is not None}, + headers={ + k: res_headers[k] + for k in [ + "Cache-Control", + "Content-Location", + "Date", + "ETag", + "Expires", + "Vary", + ] + if k in res_headers + }, ) # ETagとLast-Modifiedの検証 if if_none_match is not None: From c45f9ce69b4599a8fdd116b5b35ed42ef7e6871d Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 7 Jun 2024 19:53:03 +0900 Subject: [PATCH 22/82] =?UTF-8?q?FIX:=20=E3=82=A8=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba --- voicevox_engine/app/routers/speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 7f202a9a5..29c9d224a 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -13,7 +13,7 @@ from voicevox_engine.metas.MetasStore import MetasStore, filter_speakers_and_styles from voicevox_engine.resource_manager import ResourceManager -RESOURCE_ENDPOINT = "resources" +RESOURCE_ENDPOINT = "_resources" async def get_resource_baseurl(request: Request) -> str: From 8559434892591dd8a6de7c1228abd93da7d1f137 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 7 Jun 2024 19:58:08 +0900 Subject: [PATCH 23/82] =?UTF-8?q?FIX:=20=E6=A4=9C=E8=A8=BC=E3=81=AE?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=A8=E3=83=98=E3=83=83=E3=83=80=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 50 +++----------------------- 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 29c9d224a..4d9236847 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -1,11 +1,10 @@ """話者情報機能を提供する API Router""" -from email.utils import parsedate from pathlib import Path from typing import Annotated, Literal -from fastapi import APIRouter, Depends, Header, HTTPException, Request -from fastapi.responses import FileResponse, Response +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.responses import FileResponse from pydantic import parse_obj_as from voicevox_engine.core.core_initializer import CoreManager @@ -181,55 +180,16 @@ def singer_info( # リソースはAPIとしてアクセスするものではないことを表明するためOpenAPIスキーマーから除外する @router.get(f"/{RESOURCE_ENDPOINT}/{{resource_name}}", include_in_schema=False) - def resources( - resource_name: str, - if_none_match: Annotated[str | None, Header()] = None, - if_modified_since: Annotated[str | None, Header()] = None, - ) -> Response: + async def resources(resource_name: str) -> FileResponse: """ ResourceManagerから発行されたURLへのアクセスに対応する """ resource_path = resource_manager.resource_path(resource_name) if resource_path is None or not resource_path.exists(): raise HTTPException(status_code=404) - response = FileResponse( + return FileResponse( resource_path, - headers={ - "Cache-Control": "max-age=2592000, immutable, stale-while-revalidate=2592000" - }, - stat_result=resource_path.stat(), + headers={"Cache-Control": "max-age=2592000"}, ) - res_headers = response.headers - # 304レスポンスの作成 - modified_response = Response( - status_code=304, - headers={ - k: res_headers[k] - for k in [ - "Cache-Control", - "Content-Location", - "Date", - "ETag", - "Expires", - "Vary", - ] - if k in res_headers - }, - ) - # ETagとLast-Modifiedの検証 - if if_none_match is not None: - etag = res_headers["ETag"] - if etag in [tag.strip(" W/") for tag in if_none_match.split(",")]: - return modified_response - elif if_modified_since is not None: - _if_modified_since = parsedate(if_modified_since) - last_modified = parsedate(res_headers["Last-Modified"]) - if ( - _if_modified_since is not None - and last_modified is not None - and _if_modified_since >= last_modified - ): - return modified_response - return response return router From 3f7c5634d331a22b817257b3e26570763bb9fc67 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 10 Jun 2024 18:29:46 +0900 Subject: [PATCH 24/82] MNT: `Literal["base64", "url"]` to `ResourceFormat` --- voicevox_engine/app/routers/speaker.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 4d9236847..6a0fd24d2 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -1,7 +1,7 @@ """話者情報機能を提供する API Router""" from pathlib import Path -from typing import Annotated, Literal +from typing import Annotated, Literal, TypeAlias from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import FileResponse @@ -13,6 +13,7 @@ from voicevox_engine.resource_manager import ResourceManager RESOURCE_ENDPOINT = "_resources" +ResourceFormat: TypeAlias = Literal["base64", "url"] async def get_resource_baseurl(request: Request) -> str: @@ -39,7 +40,7 @@ def speakers(core_version: str | None = None) -> list[Speaker]: def speaker_info( resource_baseurl: Annotated[str, Depends(get_resource_baseurl)], speaker_uuid: str, - resource_format: Literal["base64", "url"] = "base64", + resource_format: ResourceFormat = "base64", core_version: str | None = None, ) -> SpeakerInfo: """ @@ -60,7 +61,7 @@ def _speaker_info( speaker_or_singer: Literal["speaker", "singer"], core_version: str | None, resource_baseurl: str, - resource_format: Literal["base64", "url"], + resource_format: ResourceFormat, ) -> SpeakerInfo: # エンジンに含まれる話者メタ情報は、次のディレクトリ構造に従わなければならない: # {root_dir}/ @@ -163,7 +164,7 @@ def singers(core_version: str | None = None) -> list[Speaker]: def singer_info( resource_baseurl: Annotated[str, Depends(get_resource_baseurl)], speaker_uuid: str, - resource_format: Literal["base64", "url"] = "base64", + resource_format: ResourceFormat = "base64", core_version: str | None = None, ) -> SpeakerInfo: """ From 94a10068b0011c17ec84824b109f11c69bf3765c Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 10 Jun 2024 18:51:07 +0900 Subject: [PATCH 25/82] FIX: fix `file_map.py` --- build_util/file_map.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/build_util/file_map.py b/build_util/file_map.py index c3554c0f7..f9aa02199 100644 --- a/build_util/file_map.py +++ b/build_util/file_map.py @@ -22,17 +22,10 @@ def make_hash(file: Path) -> str: return digest.hex() -def walk_target_dir( - target_dir: Path, suffix: tuple[str, ...] -) -> Generator[tuple[Path, str], None, None]: +def walk_target_dir(target_dir: Path) -> Generator[Path, None, None]: for root, _, files in os.walk(target_dir): for file in files: - filepath = Path(root, file) - if not filepath.suffix.endswith(suffix): - continue - filehash = make_hash(filepath) - relative = filepath.relative_to(target_dir) - yield (relative, filehash) + yield Path(root, file) def generate_path_to_hash_dict( @@ -40,8 +33,9 @@ def generate_path_to_hash_dict( ) -> dict[str, str]: suffix = tuple(target_suffix) return { - to_posix_str_path(filepath): filehash - for filepath, filehash in walk_target_dir(target_dir, suffix) + to_posix_str_path(filepath.relative_to(target_dir)): make_hash(filepath) + for filepath in walk_target_dir(target_dir) + if filepath.suffix.endswith(suffix) } From a773489a5b594b723dd74ecb17ac41f6fb44b640 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 10 Jun 2024 18:56:29 +0900 Subject: [PATCH 26/82] MNT: `file_map.py` to `generate_filemap.py` --- build_util/{file_map.py => generate_filemap.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename build_util/{file_map.py => generate_filemap.py} (100%) diff --git a/build_util/file_map.py b/build_util/generate_filemap.py similarity index 100% rename from build_util/file_map.py rename to build_util/generate_filemap.py From 894bd3a34eb4b79d9649307508e6fd2d83bf5290 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:48:08 +0900 Subject: [PATCH 27/82] FIX: `resource_manager.py` --- voicevox_engine/resource_manager.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 1dd1adb9b..dcbfb144d 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -1,3 +1,9 @@ +""" +URLから取得可能なリソースの管理 + +アップデートでリソースの変更が反映されるようにURLはハッシュ値から生成する +""" + import base64 import json from hashlib import sha256 @@ -10,8 +16,8 @@ def b64encode_str(s: bytes) -> str: class ResourceManager: - def __init__(self, is_development: bool) -> None: - self._is_development = is_development + def __init__(self, create_filemap_if_not_exist: bool) -> None: + self._create_filemap_if_not_exist = create_filemap_if_not_exist self._path_to_hash: dict[Path, str] = {} self._hash_to_path: dict[str, Path] = {} @@ -21,7 +27,7 @@ def register_dir(self, resource_dir: Path) -> None: data: dict[str, str] = json.loads(filemap_json.read_bytes()) self._path_to_hash |= {resource_dir / k: v for k, v in data.items()} else: - if self._is_development: + if self._create_filemap_if_not_exist: self._path_to_hash |= { i: sha256(i.read_bytes()).digest().hex() for i in resource_dir.glob("**/*") From 35c86ead759e89eb3cb171e8c80a174d575582d9 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 11 Jun 2024 00:16:08 +0900 Subject: [PATCH 28/82] =?UTF-8?q?ENH:=20=E3=83=93=E3=83=AB=E3=83=89?= =?UTF-8?q?=E6=99=82=E3=81=AB`filemap.json`=E3=82=92=E4=BD=9C=E6=88=90?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-engine-package.yml | 3 +++ Dockerfile | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/build-engine-package.yml b/.github/workflows/build-engine-package.yml index f979c3af6..c51c2e14d 100644 --- a/.github/workflows/build-engine-package.yml +++ b/.github/workflows/build-engine-package.yml @@ -445,6 +445,9 @@ jobs: # FIXME: VOICEVOX (editor) cannot build without licenses.json cp resources/engine_manifest_assets/dependency_licenses.json licenses.json + - name: Generate filemap.json + run: python build_util/generate_filemap.py --target_dir resources/character_info + - name: Build VOICEVOX ENGINE run.py run: | set -eux diff --git a/Dockerfile b/Dockerfile index 5b0d59b72..fd18f2360 100644 --- a/Dockerfile +++ b/Dockerfile @@ -258,6 +258,10 @@ RUN < Date: Wed, 12 Jun 2024 19:41:20 +0900 Subject: [PATCH 29/82] =?UTF-8?q?TST:=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/e2e/__snapshots__/test_speakers.ambr | 157 ++++++++++++++++++ ...88f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" | 26 +++ ...ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" | 26 +++ ...1a81618-b27b-40d2-b0ea-27a9ad408c4b].json" | 16 ++ ...5b2c544-660e-401e-b503-0e14c635303a].json" | 16 ++ ...88f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" | 26 +++ ...ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" | 26 +++ .../test_get_singer_info_with_url_200.json | 16 ++ .../test_get_singer_info_with_url_200.json | 26 +++ .../single_api/speaker/test_singer_info.py | 14 ++ .../single_api/speaker/test_speaker_info.py | 14 ++ test/e2e/test_speakers.py | 78 ++++++++- 12 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 test/e2e/__snapshots__/test_speakers.ambr create mode 100644 "test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" create mode 100644 "test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" create mode 100644 "test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[b1a81618-b27b-40d2-b0ea-27a9ad408c4b].json" create mode 100644 "test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[35b2c544-660e-401e-b503-0e14c635303a].json" create mode 100644 "test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" create mode 100644 "test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" create mode 100644 test/e2e/single_api/speaker/__snapshots__/test_singer_info/test_get_singer_info_with_url_200.json create mode 100644 test/e2e/single_api/speaker/__snapshots__/test_speaker_info/test_get_singer_info_with_url_200.json diff --git a/test/e2e/__snapshots__/test_speakers.ambr b/test/e2e/__snapshots__/test_speakers.ambr new file mode 100644 index 000000000..20a4eeb41 --- /dev/null +++ b/test/e2e/__snapshots__/test_speakers.ambr @@ -0,0 +1,157 @@ +# serializer version: 1 +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_5_icon] + 'MD5:517ba089e0b03f8868af2ce956f7699d' +# --- +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_5_voice_sample_0] + 'MD5:517ba089e0b03f8868af2ce956f7699d' +# --- +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_5_voice_sample_1] + 'MD5:517ba089e0b03f8868af2ce956f7699d' +# --- +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_5_voice_sample_2] + 'MD5:517ba089e0b03f8868af2ce956f7699d' +# --- +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_7_icon] + 'MD5:562ad0f61ca6dd81e89a4479f97dcd9f' +# --- +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_7_voice_sample_0] + 'MD5:562ad0f61ca6dd81e89a4479f97dcd9f' +# --- +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_7_voice_sample_1] + 'MD5:562ad0f61ca6dd81e89a4479f97dcd9f' +# --- +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_7_voice_sample_2] + 'MD5:562ad0f61ca6dd81e89a4479f97dcd9f' +# --- +# name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_portrait] + 'MD5:27777cb0883c98cd9870707005bf1faf' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_icon] + 'MD5:c6a8ddea789d8115372db31b4a76d2aa' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_portrait] + 'MD5:c6a8ddea789d8115372db31b4a76d2aa' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_voice_sample_0] + 'MD5:c6a8ddea789d8115372db31b4a76d2aa' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_voice_sample_1] + 'MD5:c6a8ddea789d8115372db31b4a76d2aa' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_voice_sample_2] + 'MD5:c6a8ddea789d8115372db31b4a76d2aa' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_icon] + 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_portrait] + 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_voice_sample_0] + 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_voice_sample_1] + 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_voice_sample_2] + 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' +# --- +# name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_portrait] + 'MD5:78228648fe527ee23597b66db59a4f94' +# --- +# name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_9_icon] + 'MD5:1e3d0a6e88264b1b99839aeb7c7cbe1f' +# --- +# name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_9_voice_sample_0] + 'MD5:1e3d0a6e88264b1b99839aeb7c7cbe1f' +# --- +# name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_9_voice_sample_1] + 'MD5:1e3d0a6e88264b1b99839aeb7c7cbe1f' +# --- +# name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_9_voice_sample_2] + 'MD5:1e3d0a6e88264b1b99839aeb7c7cbe1f' +# --- +# name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_portrait] + 'MD5:381ba07cbcad95f1c99109f5f6d096a8' +# --- +# name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_icon] + 'MD5:1fe576e75458c752cfeecc1e93a29886' +# --- +# name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_portrait] + 'MD5:1fe576e75458c752cfeecc1e93a29886' +# --- +# name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_voice_sample_0] + 'MD5:1fe576e75458c752cfeecc1e93a29886' +# --- +# name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_voice_sample_1] + 'MD5:1fe576e75458c752cfeecc1e93a29886' +# --- +# name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_voice_sample_2] + 'MD5:1fe576e75458c752cfeecc1e93a29886' +# --- +# name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_portrait] + 'MD5:bd6cf66dcc652f56892b14b423f6f37c' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_1_icon] + 'MD5:becb1cc2aaf82623a13a1250a39d7393' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_1_voice_sample_0] + 'MD5:becb1cc2aaf82623a13a1250a39d7393' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_1_voice_sample_1] + 'MD5:becb1cc2aaf82623a13a1250a39d7393' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_1_voice_sample_2] + 'MD5:becb1cc2aaf82623a13a1250a39d7393' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_icon] + 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_portrait] + 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_voice_sample_0] + 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_voice_sample_1] + 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_voice_sample_2] + 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' +# --- +# name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_portrait] + 'MD5:27777cb0883c98cd9870707005bf1faf' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_icon] + 'MD5:1f1da5f25968c638a783bf6ba9df9420' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_portrait] + 'MD5:1f1da5f25968c638a783bf6ba9df9420' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_voice_sample_0] + 'MD5:1f1da5f25968c638a783bf6ba9df9420' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_voice_sample_1] + 'MD5:1f1da5f25968c638a783bf6ba9df9420' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_voice_sample_2] + 'MD5:1f1da5f25968c638a783bf6ba9df9420' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_icon] + 'MD5:0b158046338f60a53e9afdb7797c5864' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_portrait] + 'MD5:0b158046338f60a53e9afdb7797c5864' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_voice_sample_0] + 'MD5:0b158046338f60a53e9afdb7797c5864' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_voice_sample_1] + 'MD5:0b158046338f60a53e9afdb7797c5864' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_voice_sample_2] + 'MD5:0b158046338f60a53e9afdb7797c5864' +# --- +# name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_portrait] + 'MD5:78228648fe527ee23597b66db59a4f94' +# --- diff --git "a/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" new file mode 100644 index 000000000..2e846f994 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" @@ -0,0 +1,26 @@ +{ + "policy": "dummy2 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "http://testserver/_resources/934d5c0f33e027f676543ab7e2b50be9c1abe7e5120b7d0a3b34bdd44efd82ae", + "style_infos": [ + { + "icon": "http://testserver/_resources/04cdf27edb6a25e1ba2a0ced57d404151e8d556540f67297ea127984e9ad144b", + "id": 5, + "portrait": null, + "voice_samples": [ + "http://testserver/_resources/8263a333e29381dc24f7412d6b18417b309ce679c446c0dd09aea80a9a9e2c34", + "http://testserver/_resources/5d53a9a385787b822295047093bb9f0745c4cecf1d46957fa392ddb2f1561aa4", + "http://testserver/_resources/da3922b08340941c6fa8f5cb8c1f55093becba8636d5b39e98694780e34bc066" + ] + }, + { + "icon": "http://testserver/_resources/cc23bf07fcf085fbea4015cc7f0e52a67fa0f2c5e6428fb537837fce9b19a6a3", + "id": 7, + "portrait": null, + "voice_samples": [ + "http://testserver/_resources/1db7a4e6530458e5e8998d8f83ade8f3bf9156853c465c16caf1b25ccfc17bdb", + "http://testserver/_resources/8c79bf75f66c9b81c48d251ab34f8bd4bfc28c381a882bdb18053e22e08c7f3b", + "http://testserver/_resources/26a3efd18b612f26186d4b8b6850fe252f43a66607c25d40f4c1d9edc73b4c21" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" new file mode 100644 index 000000000..ab2c3e82a --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" @@ -0,0 +1,26 @@ +{ + "policy": "dummy1 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "http://testserver/_resources/bc1db0b6614d4433dc98b4ab3bc7b81954471c16633f8c49c15532fabfad7e27", + "style_infos": [ + { + "icon": "http://testserver/_resources/a7eeaee45a7ad967e43af7fbc2f006e0a889252083440c85d3ce5d5b525a0be8", + "id": 4, + "portrait": "http://testserver/_resources/541554b529bb689f0c1c3bd6b606b9885583567039f42a61b0b495a96e0450f9", + "voice_samples": [ + "http://testserver/_resources/41f0f4dd473fbd42a68f4dffa90eae79d77f26b82c534b503e22d46e0202ed0e", + "http://testserver/_resources/8ab50e880fba92355649d4eb866fb39635c9d56fc22b1b58db0c110ac143328c", + "http://testserver/_resources/a5f6299bf35c6aa422b4be49d06152289509a436ee7553728d5b68c5fc1ac880" + ] + }, + { + "icon": "http://testserver/_resources/b1697caeef457c1b46e301c3313ac8ea27735e5313a47d92c0a01cde76ab11cf", + "id": 6, + "portrait": "http://testserver/_resources/5c521d46f5e73788cf26b21fa43c6c898f731c3a88b8c23f31bc7ebd83ca788e", + "voice_samples": [ + "http://testserver/_resources/891f871e89ffb2d5dc8e0483613d81b576221660f00a32893700f964552f5e11", + "http://testserver/_resources/2143f339114bad85ff9ff6253a7b0cc6453e7f6268c3052ed29dba3a810a891a", + "http://testserver/_resources/81c8222bbb4778e5e9c9e94adfa3cc1c041fa9ff39181f8a644371ca6087bae0" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[b1a81618-b27b-40d2-b0ea-27a9ad408c4b].json" "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[b1a81618-b27b-40d2-b0ea-27a9ad408c4b].json" new file mode 100644 index 000000000..5d9238da7 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[b1a81618-b27b-40d2-b0ea-27a9ad408c4b].json" @@ -0,0 +1,16 @@ +{ + "policy": "dummy4 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "http://testserver/_resources/18739f6fb92ddcf2e7070737c1d763bc2c5daf1efffc6c012e2bd1720e67abb3", + "style_infos": [ + { + "icon": "http://testserver/_resources/531e97491ca6ecc0e664475b501d5db613d4ac1019f92078ed39c08ad8000ff9", + "id": 9, + "portrait": null, + "voice_samples": [ + "http://testserver/_resources/6edf286a07f2bd462bf8fdb89a8976a3878f3c2f60a90a18425c9371002df7b5", + "http://testserver/_resources/00a74dc38ea0b1130ecda9fdad3e26d4e1a3efbdfa00a6a2c4d393aecc4a4683", + "http://testserver/_resources/83988f1e492cc0bfdd5ebfb801fdec7b5f5be6f8e3a56bba9abf39c418ed5fdb" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[35b2c544-660e-401e-b503-0e14c635303a].json" "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[35b2c544-660e-401e-b503-0e14c635303a].json" new file mode 100644 index 000000000..12c5c306f --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[35b2c544-660e-401e-b503-0e14c635303a].json" @@ -0,0 +1,16 @@ +{ + "policy": "dummy3 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "http://testserver/_resources/06112cbb75adcee7173751694dafd9d05edd90d5f230445290cb51a34caea8d1", + "style_infos": [ + { + "icon": "http://testserver/_resources/759b5b5565e369741809631fcb3bc574c16da011b07d2d8dd9ceaa149d9e139b", + "id": 8, + "portrait": "http://testserver/_resources/9ecaf2beed142fdf6a48b4a8c3726fa2864ef5c1ada4884df2b94fb07685fb14", + "voice_samples": [ + "http://testserver/_resources/7a6a56cb9d571cfb774ac2c3ca8b873e25f68624ede21d6bf2ff625b44245ab5", + "http://testserver/_resources/83fadd140b6b53938654825fa9b8ccd08f235b07de98ddec8c653fc9652f31f6", + "http://testserver/_resources/9a243a14f9b4e20193708598cb76da5dba3a42ebd1f0ad008e0f3205da95bc33" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" new file mode 100644 index 000000000..0a04d5be8 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" @@ -0,0 +1,26 @@ +{ + "policy": "dummy2 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "http://testserver/_resources/934d5c0f33e027f676543ab7e2b50be9c1abe7e5120b7d0a3b34bdd44efd82ae", + "style_infos": [ + { + "icon": "http://testserver/_resources/71080a525a7629916416d7d3650122fc13ed6539930087372bc56bcda0b3a7b6", + "id": 1, + "portrait": null, + "voice_samples": [ + "http://testserver/_resources/b9b38572be4ef4e05d0d3e763c7a2002b848a33b0fe3fcf7cbc283c489539f56", + "http://testserver/_resources/0867698c258361d6811e800de70c12621865b006e7f4a8ddda70e78d9b43ab36", + "http://testserver/_resources/d70aeae2463763fd4abf93f06305b465c54690922db49427baef14e90fab3bb1" + ] + }, + { + "icon": "http://testserver/_resources/2e4d6acdd84cf9559754f073d7d34ebb3044189e2ad01cbed5cbd1e578fdf8ad", + "id": 3, + "portrait": "http://testserver/_resources/ebe0eb2e71802519f123ddb25debc504ba28b35d8e216667df634327182931d1", + "voice_samples": [ + "http://testserver/_resources/47d116ee6dc96b5fabc2d545287835258fbf4fec2becae5ccdf85c126193637e", + "http://testserver/_resources/df93bb64533008f7165957636ca414a5d898c972065452fb2e8e013c5b56effb", + "http://testserver/_resources/660088d6b340f779f7e95976ba7f9ad7d482d4de51aeb2fc2a3a9f99715f863f" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" new file mode 100644 index 000000000..dc6cd1b8d --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222URL\343\201\247\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" @@ -0,0 +1,26 @@ +{ + "policy": "dummy1 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "http://testserver/_resources/bc1db0b6614d4433dc98b4ab3bc7b81954471c16633f8c49c15532fabfad7e27", + "style_infos": [ + { + "icon": "http://testserver/_resources/6b7f0d06aa7306be8f887a8e99fa23dc6e0cb7be80cbbe2ce2823fc304a1c350", + "id": 0, + "portrait": "http://testserver/_resources/62071bcc940cba30d87bf1a617482a0987a96954d307fe9ecd34d913f679fa2a", + "voice_samples": [ + "http://testserver/_resources/2926d3d4670d0be0eae498e8b0422ae1f85a44f7cd3df51bd70c8b8c9f607c0a", + "http://testserver/_resources/4ad32dd8e4793fa610160a3c4ef9e026a874a8399d4c86f897e2584bba171dc4", + "http://testserver/_resources/25158f0e0d2b2da3985a7ef5e2d74b46c517d3c6f6240fc3ef42be780634889b" + ] + }, + { + "icon": "http://testserver/_resources/7706521aeee094e95c0e5fecae4f1253361c60900c378403e89b3142523b7d2c", + "id": 2, + "portrait": "http://testserver/_resources/c038f14a8b3916a6d8723cfd72e949b947ff860e9c7536ad41a7f849de2f2031", + "voice_samples": [ + "http://testserver/_resources/89b799594937ae9df49e3b7caffed2392ac9432b4e30273ba09f47de1ec4e31c", + "http://testserver/_resources/7d47428eb2e765aedc0e040873a926ebbfc19c6bb8c07f0dd5e2181003d57481", + "http://testserver/_resources/106106a9d7bcba3862f79660a0295a46cd32ec4f517405a9d18f595f93cfb540" + ] + } + ] +} diff --git a/test/e2e/single_api/speaker/__snapshots__/test_singer_info/test_get_singer_info_with_url_200.json b/test/e2e/single_api/speaker/__snapshots__/test_singer_info/test_get_singer_info_with_url_200.json new file mode 100644 index 000000000..5d9238da7 --- /dev/null +++ b/test/e2e/single_api/speaker/__snapshots__/test_singer_info/test_get_singer_info_with_url_200.json @@ -0,0 +1,16 @@ +{ + "policy": "dummy4 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "http://testserver/_resources/18739f6fb92ddcf2e7070737c1d763bc2c5daf1efffc6c012e2bd1720e67abb3", + "style_infos": [ + { + "icon": "http://testserver/_resources/531e97491ca6ecc0e664475b501d5db613d4ac1019f92078ed39c08ad8000ff9", + "id": 9, + "portrait": null, + "voice_samples": [ + "http://testserver/_resources/6edf286a07f2bd462bf8fdb89a8976a3878f3c2f60a90a18425c9371002df7b5", + "http://testserver/_resources/00a74dc38ea0b1130ecda9fdad3e26d4e1a3efbdfa00a6a2c4d393aecc4a4683", + "http://testserver/_resources/83988f1e492cc0bfdd5ebfb801fdec7b5f5be6f8e3a56bba9abf39c418ed5fdb" + ] + } + ] +} diff --git a/test/e2e/single_api/speaker/__snapshots__/test_speaker_info/test_get_singer_info_with_url_200.json b/test/e2e/single_api/speaker/__snapshots__/test_speaker_info/test_get_singer_info_with_url_200.json new file mode 100644 index 000000000..0a04d5be8 --- /dev/null +++ b/test/e2e/single_api/speaker/__snapshots__/test_speaker_info/test_get_singer_info_with_url_200.json @@ -0,0 +1,26 @@ +{ + "policy": "dummy2 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "http://testserver/_resources/934d5c0f33e027f676543ab7e2b50be9c1abe7e5120b7d0a3b34bdd44efd82ae", + "style_infos": [ + { + "icon": "http://testserver/_resources/71080a525a7629916416d7d3650122fc13ed6539930087372bc56bcda0b3a7b6", + "id": 1, + "portrait": null, + "voice_samples": [ + "http://testserver/_resources/b9b38572be4ef4e05d0d3e763c7a2002b848a33b0fe3fcf7cbc283c489539f56", + "http://testserver/_resources/0867698c258361d6811e800de70c12621865b006e7f4a8ddda70e78d9b43ab36", + "http://testserver/_resources/d70aeae2463763fd4abf93f06305b465c54690922db49427baef14e90fab3bb1" + ] + }, + { + "icon": "http://testserver/_resources/2e4d6acdd84cf9559754f073d7d34ebb3044189e2ad01cbed5cbd1e578fdf8ad", + "id": 3, + "portrait": "http://testserver/_resources/ebe0eb2e71802519f123ddb25debc504ba28b35d8e216667df634327182931d1", + "voice_samples": [ + "http://testserver/_resources/47d116ee6dc96b5fabc2d545287835258fbf4fec2becae5ccdf85c126193637e", + "http://testserver/_resources/df93bb64533008f7165957636ca414a5d898c972065452fb2e8e013c5b56effb", + "http://testserver/_resources/660088d6b340f779f7e95976ba7f9ad7d482d4de51aeb2fc2a3a9f99715f863f" + ] + } + ] +} diff --git a/test/e2e/single_api/speaker/test_singer_info.py b/test/e2e/single_api/speaker/test_singer_info.py index b544e3e76..92ef880d9 100644 --- a/test/e2e/single_api/speaker/test_singer_info.py +++ b/test/e2e/single_api/speaker/test_singer_info.py @@ -16,3 +16,17 @@ def test_get_singer_info_200( ) assert response.status_code == 200 assert snapshot_json == hash_long_string(response.json()) + + +def test_get_singer_info_with_url_200( + client: TestClient, snapshot_json: SnapshotAssertion +) -> None: + response = client.get( + "/singer_info", + params={ + "speaker_uuid": "b1a81618-b27b-40d2-b0ea-27a9ad408c4b", + "resource_format": "url", + }, + ) + assert response.status_code == 200 + assert snapshot_json == hash_long_string(response.json()) diff --git a/test/e2e/single_api/speaker/test_speaker_info.py b/test/e2e/single_api/speaker/test_speaker_info.py index 24f5564c4..eb7e72962 100644 --- a/test/e2e/single_api/speaker/test_speaker_info.py +++ b/test/e2e/single_api/speaker/test_speaker_info.py @@ -16,3 +16,17 @@ def test_get_speaker_info_200( ) assert response.status_code == 200 assert snapshot_json == hash_long_string(response.json()) + + +def test_get_singer_info_with_url_200( + client: TestClient, snapshot_json: SnapshotAssertion +) -> None: + response = client.get( + "/speaker_info", + params={ + "speaker_uuid": "388f246b-8c41-4ac1-8e2d-5d79f3ff56d9", + "resource_format": "url", + }, + ) + assert response.status_code == 200 + assert snapshot_json == hash_long_string(response.json()) diff --git a/test/e2e/test_speakers.py b/test/e2e/test_speakers.py index 2c7affec3..55ab24821 100644 --- a/test/e2e/test_speakers.py +++ b/test/e2e/test_speakers.py @@ -3,17 +3,23 @@ TODO: 話者と歌手の両ドメイン共通のドメイン用語を定め、このテストファイル名を変更する。 """ +import hashlib from test.utility import hash_long_string from fastapi.testclient import TestClient from pydantic import TypeAdapter from syrupy.assertion import SnapshotAssertion -from voicevox_engine.metas.Metas import Speaker +from voicevox_engine.metas.Metas import Speaker, SpeakerInfo _speaker_list_adapter = TypeAdapter(list[Speaker]) +def _hash_bytes(value: bytes) -> str: + """バイト列をハッシュ化する""" + return "MD5:" + hashlib.md5(value).hexdigest() + + def test_話者一覧が取得できる( client: TestClient, snapshot_json: SnapshotAssertion ) -> None: @@ -35,6 +41,41 @@ def test_話者の情報を取得できる( ) == hash_long_string(response.json()) +def test_話者の情報をURLで取得できる( + client: TestClient, snapshot_json: SnapshotAssertion, snapshot: SnapshotAssertion +) -> None: + speakers = _speaker_list_adapter.validate_json(client.get("/speakers").content) + for speaker in speakers: + speaker_id = speaker.speaker_uuid + response = client.get( + "/speaker_info", + params={"speaker_uuid": speaker_id, "resource_format": "url"}, + ) + assert snapshot_json(name=speaker_id) == response.json() + speaker_info = SpeakerInfo.model_validate_json(response.content) + portrait = client.get(speaker_info.portrait) + assert portrait.status_code == 200 + assert snapshot(name=f"{speaker_id}_portrait") == _hash_bytes(portrait.content) + for style in speaker_info.style_infos: + icon = client.get(style.icon) + assert icon.status_code == 200 + assert snapshot(name=f"{speaker_id}_{style.id}_icon") == _hash_bytes( + icon.content + ) + if style.portrait is not None: + portrait = client.get(style.portrait) + assert portrait.status_code == 200 + assert snapshot( + name=f"{speaker_id}_{style.id}_portrait" + ) == _hash_bytes(icon.content) + for i, voice_sample in enumerate(style.voice_samples): + sample = client.get(voice_sample) + assert sample.status_code == 200 + assert snapshot( + name=f"{speaker_id}_{style.id}_voice_sample_{i}" + ) == _hash_bytes(icon.content) + + def test_歌手一覧が取得できる( client: TestClient, snapshot_json: SnapshotAssertion ) -> None: @@ -54,3 +95,38 @@ def test_歌手の情報を取得できる( assert snapshot_json( name=singer.speaker_uuid, ) == hash_long_string(response.json()) + + +def test_歌手の情報をURLで取得できる( + client: TestClient, snapshot_json: SnapshotAssertion, snapshot: SnapshotAssertion +) -> None: + singers = _speaker_list_adapter.validate_json(client.get("/singers").content) + for singer in singers: + singer_id = singer.speaker_uuid + response = client.get( + "/singer_info", + params={"speaker_uuid": singer_id, "resource_format": "url"}, + ) + assert snapshot_json(name=singer_id) == response.json() + speaker_info = SpeakerInfo.model_validate_json(response.content) + portrait = client.get(speaker_info.portrait) + assert portrait.status_code == 200 + assert snapshot(name=f"{singer_id}_portrait") == _hash_bytes(portrait.content) + for style in speaker_info.style_infos: + icon = client.get(style.icon) + assert icon.status_code == 200 + assert snapshot(name=f"{singer_id}_{style.id}_icon") == _hash_bytes( + icon.content + ) + if style.portrait is not None: + portrait = client.get(style.portrait) + assert portrait.status_code == 200 + assert snapshot(name=f"{singer_id}_{style.id}_portrait") == _hash_bytes( + icon.content + ) + for i, voice_sample in enumerate(style.voice_samples): + sample = client.get(voice_sample) + assert sample.status_code == 200 + assert snapshot( + name=f"{singer_id}_{style.id}_voice_sample_{i}" + ) == _hash_bytes(icon.content) From 493417d00e1c44d4fd90b9eb95884f16fa17c11c Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 14 Jun 2024 00:54:55 +0900 Subject: [PATCH 30/82] =?UTF-8?q?FIX:=20=E4=BE=8B=E5=A4=96=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=AE=E5=BC=B7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 4 ++-- voicevox_engine/resource_manager.py | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 9bb47e562..ee68ea591 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -11,7 +11,7 @@ from voicevox_engine.core.core_initializer import CoreManager from voicevox_engine.metas.Metas import Speaker, SpeakerInfo from voicevox_engine.metas.MetasStore import MetasStore, filter_speakers_and_styles -from voicevox_engine.resource_manager import ResourceManager +from voicevox_engine.resource_manager import ResourceManager, ResourceManagerError RESOURCE_ENDPOINT = "_resources" ResourceFormat: TypeAlias = Literal["base64", "url"] @@ -147,7 +147,7 @@ def _resource_str(path: Path) -> str: "voice_samples": voice_samples, } ) - except FileNotFoundError: + except (FileNotFoundError, ResourceManagerError): msg = "追加情報が見つかりませんでした" raise HTTPException(status_code=500, detail=msg) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index dcbfb144d..9ea11eaab 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -11,6 +11,11 @@ from typing import Literal +class ResourceManagerError(Exception): + def __init__(self, message: str): + self.message = message + + def b64encode_str(s: bytes) -> str: return base64.b64encode(s).decode("utf-8") @@ -34,7 +39,7 @@ def register_dir(self, resource_dir: Path) -> None: if i.is_file() } else: - raise Exception(f"{filemap_json}が見つかりません") + raise ResourceManagerError(f"{filemap_json}が見つかりません") self._hash_to_path |= {v: k for k, v in self._path_to_hash.items()} def resource_str( @@ -43,9 +48,12 @@ def resource_str( base_url: str, resource_format: Literal["base64", "url"], ) -> str: - if resource_format == "base64": - return b64encode_str(resource_path.read_bytes()) - return f"{base_url}/{self._path_to_hash[resource_path]}" + try: + if resource_format == "base64": + return b64encode_str(resource_path.read_bytes()) + return f"{base_url}/{self._path_to_hash[resource_path]}" + except (FileNotFoundError, KeyError): + raise ResourceManagerError(f"{resource_path}がfilemapに登録されていません") def resource_path(self, filehash: str) -> Path | None: return self._hash_to_path.get(filehash) From 5ae701c0c18798b1ef75411fa172f66fd1b1238a Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 14 Jun 2024 07:18:58 +0900 Subject: [PATCH 31/82] =?UTF-8?q?TST:=20=E3=83=A6=E3=83=8B=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource_manager/test_resource_manager.py | 84 ++++++++++++++++++ .../resource_manager/with_filemap/dummy.png | Bin 0 -> 67 bytes .../resource_manager/with_filemap/dummy.txt | 1 + .../resource_manager/with_filemap/dummy.wav | Bin 0 -> 44 bytes .../with_filemap/filemap.json | 1 + .../without_filemap/dummy.png | Bin 0 -> 67 bytes .../without_filemap/dummy.txt | 1 + .../without_filemap/dummy.wav | Bin 0 -> 44 bytes 8 files changed, 87 insertions(+) create mode 100644 test/unit/resource_manager/test_resource_manager.py create mode 100644 test/unit/resource_manager/with_filemap/dummy.png create mode 100644 test/unit/resource_manager/with_filemap/dummy.txt create mode 100644 test/unit/resource_manager/with_filemap/dummy.wav create mode 100644 test/unit/resource_manager/with_filemap/filemap.json create mode 100644 test/unit/resource_manager/without_filemap/dummy.png create mode 100644 test/unit/resource_manager/without_filemap/dummy.txt create mode 100644 test/unit/resource_manager/without_filemap/dummy.wav diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py new file mode 100644 index 000000000..482eadf42 --- /dev/null +++ b/test/unit/resource_manager/test_resource_manager.py @@ -0,0 +1,84 @@ +import base64 +from os.path import basename +from pathlib import Path + +import pytest + +from voicevox_engine.resource_manager import ResourceManager, ResourceManagerError + +with_filemap_dir = Path(__file__).parent / "with_filemap" +without_filemap_dir = Path(__file__).parent / "without_filemap" + +dummy_base_url = "http://localhost" + + +def b64encode_str(s: bytes) -> str: + return base64.b64encode(s).decode("utf-8") + + +def test_with_filemap() -> None: + manager = ResourceManager(False) + manager.register_dir(with_filemap_dir) + png_path = with_filemap_dir / "dummy.png" + png_bytes = png_path.read_bytes() + + assert manager.resource_str(png_path, dummy_base_url, "base64") == b64encode_str( + png_bytes + ) + png_filehash = basename(manager.resource_str(png_path, dummy_base_url, "url")) + png_res_path = manager.resource_path(png_filehash) + assert png_res_path is not None + assert png_res_path.read_bytes() == png_bytes + + wav_path = with_filemap_dir / "dummy.wav" + wav_bytes = wav_path.read_bytes() + + assert manager.resource_str(wav_path, dummy_base_url, "base64") == b64encode_str( + wav_bytes + ) + wav_filehash = basename(manager.resource_str(wav_path, dummy_base_url, "url")) + wav_res_path = manager.resource_path(wav_filehash) + assert wav_res_path is not None + assert wav_res_path.read_bytes() == wav_bytes + + txt_path = with_filemap_dir / "dummy.txt" + with pytest.raises(ResourceManagerError) as _: + manager.resource_str(txt_path, dummy_base_url, "url") + + assert manager.resource_path("BAD_HASH") is None + + +def test_without_filemap_when_production() -> None: + manager = ResourceManager(False) + with pytest.raises(ResourceManagerError) as _: + manager.register_dir(without_filemap_dir) + + +def test_without_filemap() -> None: + manager = ResourceManager(True) + manager.register_dir(without_filemap_dir) + png_path = without_filemap_dir / "dummy.png" + png_bytes = png_path.read_bytes() + + png_filehash = basename(manager.resource_str(png_path, dummy_base_url, "url")) + png_res_path = manager.resource_path(png_filehash) + assert png_res_path is not None + assert png_res_path.read_bytes() == png_bytes + + wav_path = without_filemap_dir / "dummy.wav" + wav_bytes = wav_path.read_bytes() + + wav_filehash = basename(manager.resource_str(wav_path, dummy_base_url, "url")) + wav_res_path = manager.resource_path(wav_filehash) + assert wav_res_path is not None + assert wav_res_path.read_bytes() == wav_bytes + + txt_path = without_filemap_dir / "dummy.txt" + txt_bytes = txt_path.read_bytes() + + txt_filehash = basename(manager.resource_str(txt_path, dummy_base_url, "url")) + txt_res_path = manager.resource_path(txt_filehash) + assert txt_res_path is not None + assert txt_res_path.read_bytes() == txt_bytes + + assert manager.resource_path("BAD_HASH") is None diff --git a/test/unit/resource_manager/with_filemap/dummy.png b/test/unit/resource_manager/with_filemap/dummy.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8d70f28807ee0f71c820690d493d1a5d3f5a69 GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_bPE>9Q75DwYoAN&lAjEoFs@#zg9 NSx;9#mvv4FO#o6h3%mdT literal 0 HcmV?d00001 diff --git a/test/unit/resource_manager/with_filemap/dummy.txt b/test/unit/resource_manager/with_filemap/dummy.txt new file mode 100644 index 000000000..6a4762f8c --- /dev/null +++ b/test/unit/resource_manager/with_filemap/dummy.txt @@ -0,0 +1 @@ +DUMMY-TEXT diff --git a/test/unit/resource_manager/with_filemap/dummy.wav b/test/unit/resource_manager/with_filemap/dummy.wav new file mode 100644 index 0000000000000000000000000000000000000000..31cbbee871f1630dc549642a4154df22397a7648 GIT binary patch literal 44 vcmWIYbaPW-U|9Q75DwYoAN&lAjEoFs@#zg9 NSx;9#mvv4FO#o6h3%mdT literal 0 HcmV?d00001 diff --git a/test/unit/resource_manager/without_filemap/dummy.txt b/test/unit/resource_manager/without_filemap/dummy.txt new file mode 100644 index 000000000..6a4762f8c --- /dev/null +++ b/test/unit/resource_manager/without_filemap/dummy.txt @@ -0,0 +1 @@ +DUMMY-TEXT diff --git a/test/unit/resource_manager/without_filemap/dummy.wav b/test/unit/resource_manager/without_filemap/dummy.wav new file mode 100644 index 0000000000000000000000000000000000000000..31cbbee871f1630dc549642a4154df22397a7648 GIT binary patch literal 44 vcmWIYbaPW-U| Date: Fri, 14 Jun 2024 07:35:05 +0900 Subject: [PATCH 32/82] =?UTF-8?q?ENH:=20`is=5Fdevelopment()`=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E3=81=86=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/application.py | 3 ++- voicevox_engine/utility/path_utility.py | 15 ++++----------- voicevox_engine/utility/runtime_utility.py | 12 ++++++++++++ 3 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 voicevox_engine/utility/runtime_utility.py diff --git a/voicevox_engine/app/application.py b/voicevox_engine/app/application.py index 495e29578..7c6f1fb98 100644 --- a/voicevox_engine/app/application.py +++ b/voicevox_engine/app/application.py @@ -30,6 +30,7 @@ from voicevox_engine.tts_pipeline.tts_engine import TTSEngineManager from voicevox_engine.user_dict.user_dict_manager import UserDictionary from voicevox_engine.utility.path_utility import engine_root +from voicevox_engine.utility.runtime_utility import is_development def generate_app( @@ -72,7 +73,7 @@ def generate_app( app.include_router(generate_morphing_router(tts_engines, core_manager, metas_store)) app.include_router(generate_preset_router(preset_manager)) - resource_manager = ResourceManager(True) + resource_manager = ResourceManager(is_development()) resource_manager.register_dir(speaker_info_dir) app.include_router( generate_speaker_router( diff --git a/voicevox_engine/utility/path_utility.py b/voicevox_engine/utility/path_utility.py index f99db2551..6d3fc5d06 100644 --- a/voicevox_engine/utility/path_utility.py +++ b/voicevox_engine/utility/path_utility.py @@ -5,10 +5,12 @@ from platformdirs import user_data_dir +from voicevox_engine.utility.runtime_utility import is_development + def engine_root() -> Path: """エンジンのルートディレクトリを指すパスを取得する。""" - if _is_development(): + if is_development(): # git レポジトリのルートを指している root_dir = Path(__file__).parents[2] else: @@ -28,22 +30,13 @@ def engine_manifest_path() -> Path: return engine_root() / "engine_manifest.json" -def _is_development() -> bool: - """ - 動作環境が開発版であるか否かを返す。 - Pyinstallerでコンパイルされていない場合は開発環境とする。 - """ - # pyinstallerでビルドをした際はsys.frozenが設定される - return False if getattr(sys, "frozen", False) else True - - def get_save_dir() -> Path: """ファイルの保存先ディレクトリを指すパスを取得する。""" # FIXME: ファイル保存場所をエンジン固有のIDが入ったものにする # FIXME: Windowsは`voicevox-engine/voicevox-engine`ディレクトリに保存されているので # `VOICEVOX/voicevox-engine`に変更する - if _is_development(): + if is_development(): app_name = "voicevox-engine-dev" else: app_name = "voicevox-engine" diff --git a/voicevox_engine/utility/runtime_utility.py b/voicevox_engine/utility/runtime_utility.py new file mode 100644 index 000000000..db9e6a1a8 --- /dev/null +++ b/voicevox_engine/utility/runtime_utility.py @@ -0,0 +1,12 @@ +"""実行環境に関する utility""" + +import sys + + +def is_development() -> bool: + """ + 動作環境が開発版であるか否かを返す。 + Pyinstallerでコンパイルされていない場合は開発環境とする。 + """ + # pyinstallerでビルドをした際はsys.frozenが設定される + return False if getattr(sys, "frozen", False) else True From 8e889a8d0a3d70a0a5bbcd2530bde53ba7d7c7b6 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 17 Jun 2024 07:45:24 +0900 Subject: [PATCH 33/82] =?UTF-8?q?FIX:=20`filemap.json`=E3=81=AB=E5=90=AB?= =?UTF-8?q?=E3=81=BE=E3=82=8C=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AFbase64=E3=81=AE=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=82=82=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/unit/resource_manager/test_resource_manager.py | 11 +++++++++++ voicevox_engine/resource_manager.py | 8 ++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index 482eadf42..d0099f825 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -42,6 +42,8 @@ def test_with_filemap() -> None: assert wav_res_path.read_bytes() == wav_bytes txt_path = with_filemap_dir / "dummy.txt" + with pytest.raises(ResourceManagerError) as _: + manager.resource_str(txt_path, dummy_base_url, "base64") with pytest.raises(ResourceManagerError) as _: manager.resource_str(txt_path, dummy_base_url, "url") @@ -60,6 +62,9 @@ def test_without_filemap() -> None: png_path = without_filemap_dir / "dummy.png" png_bytes = png_path.read_bytes() + assert manager.resource_str(png_path, dummy_base_url, "base64") == b64encode_str( + png_bytes + ) png_filehash = basename(manager.resource_str(png_path, dummy_base_url, "url")) png_res_path = manager.resource_path(png_filehash) assert png_res_path is not None @@ -68,6 +73,9 @@ def test_without_filemap() -> None: wav_path = without_filemap_dir / "dummy.wav" wav_bytes = wav_path.read_bytes() + assert manager.resource_str(wav_path, dummy_base_url, "base64") == b64encode_str( + wav_bytes + ) wav_filehash = basename(manager.resource_str(wav_path, dummy_base_url, "url")) wav_res_path = manager.resource_path(wav_filehash) assert wav_res_path is not None @@ -76,6 +84,9 @@ def test_without_filemap() -> None: txt_path = without_filemap_dir / "dummy.txt" txt_bytes = txt_path.read_bytes() + assert manager.resource_str(txt_path, dummy_base_url, "base64") == b64encode_str( + txt_bytes + ) txt_filehash = basename(manager.resource_str(txt_path, dummy_base_url, "url")) txt_res_path = manager.resource_path(txt_filehash) assert txt_res_path is not None diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 9ea11eaab..84ecaba5f 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -48,12 +48,12 @@ def resource_str( base_url: str, resource_format: Literal["base64", "url"], ) -> str: - try: + filehash = self._path_to_hash.get(resource_path) + if filehash is not None: if resource_format == "base64": return b64encode_str(resource_path.read_bytes()) - return f"{base_url}/{self._path_to_hash[resource_path]}" - except (FileNotFoundError, KeyError): - raise ResourceManagerError(f"{resource_path}がfilemapに登録されていません") + return f"{base_url}/{filehash}" + raise ResourceManagerError(f"{resource_path}がfilemapに登録されていません") def resource_path(self, filehash: str) -> Path | None: return self._hash_to_path.get(filehash) From fb3ec121467714dd20094671d1172dae2c2bc96a Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:17:41 +0900 Subject: [PATCH 34/82] =?UTF-8?q?FIX:=20=E3=82=A8=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E3=81=A8=E3=83=A2=E3=83=87?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E8=AA=AC=E6=98=8E=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...250\343\202\222\347\242\272\350\252\215.json" | 16 ++++++++-------- voicevox_engine/app/routers/speaker.py | 4 ++-- voicevox_engine/metas/Metas.py | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" index ea475d7b7..d0b6025bb 100644 --- "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" +++ "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" @@ -721,14 +721,14 @@ "type": "object" }, "SpeakerInfo": { - "description": "話者の追加情報", + "description": "話者の追加情報\n`リソース`はバイナリをbase64エンコードしたもの、または取得用のURL", "properties": { "policy": { "title": "policy.md", "type": "string" }, "portrait": { - "title": "portrait.pngをbase64エンコードしたもの", + "title": "ポートレート画像のリソース", "type": "string" }, "style_infos": { @@ -803,10 +803,10 @@ "type": "object" }, "StyleInfo": { - "description": "スタイルの追加情報", + "description": "スタイルの追加情報\n`リソース`はバイナリをbase64エンコードしたもの、または取得用のURL", "properties": { "icon": { - "title": "当該スタイルのアイコンをbase64エンコードしたもの", + "title": "当該スタイルのアイコンのリソース", "type": "string" }, "id": { @@ -814,14 +814,14 @@ "type": "integer" }, "portrait": { - "title": "当該スタイルのportrait.pngをbase64エンコードしたもの", + "title": "当該スタイルのポートレート画像のリソース", "type": "string" }, "voice_samples": { "items": { "type": "string" }, - "title": "voice_sampleのwavファイルをbase64エンコードしたもの", + "title": "サンプル音声のリソース", "type": "array" } }, @@ -2459,7 +2459,7 @@ }, "/singer_info": { "get": { - "description": "指定されたspeaker_uuidの歌手に関する情報をjson形式で返します。\n画像や音声はbase64エンコードされたものが返されます。", + "description": "指定されたspeaker_uuidの歌手に関する情報をjson形式で返します。\n画像や音声はresource_formatで指定した形式で返されます。", "operationId": "singer_info_singer_info_get", "parameters": [ { @@ -2572,7 +2572,7 @@ }, "/speaker_info": { "get": { - "description": "指定されたspeaker_uuidの話者に関する情報をjson形式で返します。\n画像や音声はbase64エンコードされたものが返されます。", + "description": "指定されたspeaker_uuidの話者に関する情報をjson形式で返します。\n画像や音声はresource_formatで指定した形式で返されます。", "operationId": "speaker_info_speaker_info_get", "parameters": [ { diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index ee68ea591..ee818cfda 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -46,7 +46,7 @@ def speaker_info( ) -> SpeakerInfo: """ 指定されたspeaker_uuidの話者に関する情報をjson形式で返します。 - 画像や音声はbase64エンコードされたものが返されます。 + 画像や音声はresource_formatで指定した形式で返されます。 """ return _speaker_info( speaker_uuid=speaker_uuid, @@ -172,7 +172,7 @@ def singer_info( ) -> SpeakerInfo: """ 指定されたspeaker_uuidの歌手に関する情報をjson形式で返します。 - 画像や音声はbase64エンコードされたものが返されます。 + 画像や音声はresource_formatで指定した形式で返されます。 """ return _speaker_info( speaker_uuid=speaker_uuid, diff --git a/voicevox_engine/metas/Metas.py b/voicevox_engine/metas/Metas.py index 208dd0007..9596b094f 100644 --- a/voicevox_engine/metas/Metas.py +++ b/voicevox_engine/metas/Metas.py @@ -69,23 +69,23 @@ class Speaker(BaseModel): class StyleInfo(BaseModel): """ スタイルの追加情報 + `リソース`はバイナリをbase64エンコードしたもの、または取得用のURL """ id: StyleId = Field(title="スタイルID") - icon: str = Field(title="当該スタイルのアイコンをbase64エンコードしたもの") + icon: str = Field(title="当該スタイルのアイコンのリソース") portrait: str | SkipJsonSchema[None] = Field( - default=None, title="当該スタイルのportrait.pngをbase64エンコードしたもの" - ) - voice_samples: list[str] = Field( - title="voice_sampleのwavファイルをbase64エンコードしたもの" + default=None, title="当該スタイルのポートレート画像のリソース" ) + voice_samples: list[str] = Field(title="サンプル音声のリソース") class SpeakerInfo(BaseModel): """ 話者の追加情報 + `リソース`はバイナリをbase64エンコードしたもの、または取得用のURL """ policy: str = Field(title="policy.md") - portrait: str = Field(title="portrait.pngをbase64エンコードしたもの") + portrait: str = Field(title="ポートレート画像のリソース") style_infos: list[StyleInfo] = Field(title="スタイルの追加情報") From 42c25860a5e103c2c4e399efe96622977f10a626 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:19:15 +0900 Subject: [PATCH 35/82] Update test/e2e/test_speakers.py Co-authored-by: Hiroshiba --- test/e2e/test_speakers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/test_speakers.py b/test/e2e/test_speakers.py index 55ab24821..bfbfeca5b 100644 --- a/test/e2e/test_speakers.py +++ b/test/e2e/test_speakers.py @@ -52,10 +52,12 @@ def test_話者の情報をURLで取得できる( params={"speaker_uuid": speaker_id, "resource_format": "url"}, ) assert snapshot_json(name=speaker_id) == response.json() + speaker_info = SpeakerInfo.model_validate_json(response.content) portrait = client.get(speaker_info.portrait) assert portrait.status_code == 200 assert snapshot(name=f"{speaker_id}_portrait") == _hash_bytes(portrait.content) + for style in speaker_info.style_infos: icon = client.get(style.icon) assert icon.status_code == 200 From 0a6bb3815b62bb2fb47b384714a37769b6619667 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:20:16 +0900 Subject: [PATCH 36/82] Update build_util/generate_filemap.py Co-authored-by: Hiroshiba --- build_util/generate_filemap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_util/generate_filemap.py b/build_util/generate_filemap.py index f9aa02199..a856f4af2 100644 --- a/build_util/generate_filemap.py +++ b/build_util/generate_filemap.py @@ -1,5 +1,5 @@ """ -'ResourceManager'が参照するファイルを生成 +'ResourceManager'が参照するfilemapを予め生成する。 """ import json From a4ce2adbed3a4a1beeeff76ee623a67a4197a01a Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:22:41 +0900 Subject: [PATCH 37/82] Update build_util/generate_filemap.py Co-authored-by: Hiroshiba --- build_util/generate_filemap.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build_util/generate_filemap.py b/build_util/generate_filemap.py index a856f4af2..f744c83cf 100644 --- a/build_util/generate_filemap.py +++ b/build_util/generate_filemap.py @@ -50,12 +50,14 @@ def main() -> None: default=DEFAULT_TARGET_SUFFIX, help=f"filemapに登録するファイルの拡張子\nデフォルトは{', '.join(DEFAULT_TARGET_SUFFIX)}", ) - arg = parser.parse_args() - target_dir: Path = arg.target_dir + args = parser.parse_args() + + target_dir: Path = args.target_dir if not target_dir.is_dir(): raise Exception(f"{target_dir}はディレクトリではありません") + save_path = target_dir / DEFAULT_FILENAME - path_to_hash = generate_path_to_hash_dict(target_dir, arg.target_suffix) + path_to_hash = generate_path_to_hash_dict(target_dir, args.target_suffix) save_path.write_text(json.dumps(path_to_hash, ensure_ascii=False), encoding="utf-8") From 58083170c03efa08f2b8910b99e6092c66e82631 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:26:19 +0900 Subject: [PATCH 38/82] =?UTF-8?q?FIX:=20`main()`=E3=82=92=E5=BB=83?= =?UTF-8?q?=E6=AD=A2=E3=81=97=E3=81=A6`if=20=5F=5Fname=5F=5F=20=3D=3D=20"?= =?UTF-8?q?=5F=5Fmain=5F=5F":`=E3=81=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_util/generate_filemap.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/build_util/generate_filemap.py b/build_util/generate_filemap.py index f744c83cf..563dae93c 100644 --- a/build_util/generate_filemap.py +++ b/build_util/generate_filemap.py @@ -39,7 +39,7 @@ def generate_path_to_hash_dict( } -def main() -> None: +if __name__ == "__main__": parser = ArgumentParser() parser.add_argument( "--target_dir", type=Path, required=True, help="filemapを作成するディレクトリ" @@ -59,7 +59,3 @@ def main() -> None: save_path = target_dir / DEFAULT_FILENAME path_to_hash = generate_path_to_hash_dict(target_dir, args.target_suffix) save_path.write_text(json.dumps(path_to_hash, ensure_ascii=False), encoding="utf-8") - - -if __name__ == "__main__": - main() From 3511d62ebd1a4695f236f8e3c59abe030d31dbb1 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:35:43 +0900 Subject: [PATCH 39/82] Update voicevox_engine/resource_manager.py Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 84ecaba5f..018f06e08 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -21,6 +21,13 @@ def b64encode_str(s: bytes) -> str: class ResourceManager: + """ + リソースファイルのパスと、一意なハッシュ値の対応(filemap)を管理する。 + + APIでリソースファイルを一意なURLとして返すときに使う。 + ついでにファイルをbase64文字列に変換することもできる。 + """ + def __init__(self, create_filemap_if_not_exist: bool) -> None: self._create_filemap_if_not_exist = create_filemap_if_not_exist self._path_to_hash: dict[Path, str] = {} From d9a28e0ac78fd6fdb50f2ffc64dc33b0e88ce67c Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:58:30 +0900 Subject: [PATCH 40/82] Update voicevox_engine/resource_manager.py Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 018f06e08..eee4e2019 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -34,6 +34,7 @@ def __init__(self, create_filemap_if_not_exist: bool) -> None: self._hash_to_path: dict[str, Path] = {} def register_dir(self, resource_dir: Path) -> None: + """ディレクトリをfilemapに登録する""" filemap_json = resource_dir / "filemap.json" if filemap_json.exists(): data: dict[str, str] = json.loads(filemap_json.read_bytes()) From ade94ff194f085717edfdbf5b06450be554592f9 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:00:10 +0900 Subject: [PATCH 41/82] Update voicevox_engine/resource_manager.py Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index eee4e2019..6ef19a87b 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -43,7 +43,7 @@ def register_dir(self, resource_dir: Path) -> None: if self._create_filemap_if_not_exist: self._path_to_hash |= { i: sha256(i.read_bytes()).digest().hex() - for i in resource_dir.glob("**/*") + for i in resource_dir.rglob("*") if i.is_file() } else: From 662ce3714b67cfdc1ff14975b686171b8b925023 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:17:48 +0900 Subject: [PATCH 42/82] Update voicevox_engine/resource_manager.py Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 6ef19a87b..231646346 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -57,11 +57,12 @@ def resource_str( resource_format: Literal["base64", "url"], ) -> str: filehash = self._path_to_hash.get(resource_path) - if filehash is not None: - if resource_format == "base64": - return b64encode_str(resource_path.read_bytes()) - return f"{base_url}/{filehash}" - raise ResourceManagerError(f"{resource_path}がfilemapに登録されていません") + if filehash is None: + raise ResourceManagerError(f"{resource_path}がfilemapに登録されていません") + + if resource_format == "base64": + return b64encode_str(resource_path.read_bytes()) + return f"{base_url}/{filehash}" def resource_path(self, filehash: str) -> Path | None: return self._hash_to_path.get(filehash) From afffa0d8538149c04fe2ea294c99dc23b7db8794 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:03:34 +0900 Subject: [PATCH 43/82] FIX: `register_dir` --- voicevox_engine/resource_manager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 231646346..551a3cdce 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -39,15 +39,15 @@ def register_dir(self, resource_dir: Path) -> None: if filemap_json.exists(): data: dict[str, str] = json.loads(filemap_json.read_bytes()) self._path_to_hash |= {resource_dir / k: v for k, v in data.items()} + elif self._create_filemap_if_not_exist: + self._path_to_hash |= { + i: sha256(i.read_bytes()).digest().hex() + for i in resource_dir.rglob("*") + if i.is_file() + } else: - if self._create_filemap_if_not_exist: - self._path_to_hash |= { - i: sha256(i.read_bytes()).digest().hex() - for i in resource_dir.rglob("*") - if i.is_file() - } - else: - raise ResourceManagerError(f"{filemap_json}が見つかりません") + raise ResourceManagerError(f"{filemap_json}が見つかりません") + self._hash_to_path |= {v: k for k, v in self._path_to_hash.items()} def resource_str( From 1a3fed1c21922630c4324728ff5b22c26d31976e Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:43:26 +0900 Subject: [PATCH 44/82] Update voicevox_engine/resource_manager.py Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 551a3cdce..4d187d10c 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -1,7 +1,5 @@ """ -URLから取得可能なリソースの管理 - -アップデートでリソースの変更が反映されるようにURLはハッシュ値から生成する +リソースファイルを管理する。 """ import base64 From 9620310722edce09d740db8bad8e9ec16b27b70d Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:45:52 +0900 Subject: [PATCH 45/82] Update voicevox_engine/resource_manager.py Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 4d187d10c..9613c158c 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -63,4 +63,5 @@ def resource_str( return f"{base_url}/{filehash}" def resource_path(self, filehash: str) -> Path | None: + """指定したハッシュ値を持つリソースファイルのパスを返す。""" return self._hash_to_path.get(filehash) From b0cf1b2fbb0b1d4093e7ca7b88d248af6788cfde Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:49:30 +0900 Subject: [PATCH 46/82] Update voicevox_engine/metas/Metas.py Co-authored-by: Hiroshiba --- voicevox_engine/metas/Metas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicevox_engine/metas/Metas.py b/voicevox_engine/metas/Metas.py index 9596b094f..c97d6be47 100644 --- a/voicevox_engine/metas/Metas.py +++ b/voicevox_engine/metas/Metas.py @@ -87,5 +87,5 @@ class SpeakerInfo(BaseModel): """ policy: str = Field(title="policy.md") - portrait: str = Field(title="ポートレート画像のリソース") + portrait: str = Field(title="立ち絵画像のリソース") style_infos: list[StyleInfo] = Field(title="スタイルの追加情報") From a2dbf40b19c1a0c35004ba13b867442abd1ccb5d Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:50:13 +0900 Subject: [PATCH 47/82] Update voicevox_engine/metas/Metas.py Co-authored-by: Hiroshiba --- voicevox_engine/metas/Metas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicevox_engine/metas/Metas.py b/voicevox_engine/metas/Metas.py index c97d6be47..c702493dc 100644 --- a/voicevox_engine/metas/Metas.py +++ b/voicevox_engine/metas/Metas.py @@ -75,7 +75,7 @@ class StyleInfo(BaseModel): id: StyleId = Field(title="スタイルID") icon: str = Field(title="当該スタイルのアイコンのリソース") portrait: str | SkipJsonSchema[None] = Field( - default=None, title="当該スタイルのポートレート画像のリソース" + default=None, title="このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL" ) voice_samples: list[str] = Field(title="サンプル音声のリソース") From 318ab78d65eb528392aceb2ee1410c73c6957f52 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:50:55 +0900 Subject: [PATCH 48/82] Update voicevox_engine/app/routers/speaker.py Co-authored-by: Hiroshiba --- voicevox_engine/app/routers/speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index ee818cfda..a767c4e5e 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -193,7 +193,7 @@ async def resources(resource_name: str) -> FileResponse: raise HTTPException(status_code=404) return FileResponse( resource_path, - headers={"Cache-Control": "max-age=2592000"}, + headers={"Cache-Control": "max-age=2592000"}, # 30日 ) return router From d19788559a5723aed3a649cc4313697c3bbfa912 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:51:56 +0900 Subject: [PATCH 49/82] Update voicevox_engine/app/routers/speaker.py Co-authored-by: Hiroshiba --- voicevox_engine/app/routers/speaker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index a767c4e5e..40f026070 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -184,9 +184,9 @@ def singer_info( # リソースはAPIとしてアクセスするものではないことを表明するためOpenAPIスキーマーから除外する @router.get(f"/{RESOURCE_ENDPOINT}/{{resource_name}}", include_in_schema=False) - async def resources(resource_name: str) -> FileResponse: + async def resources(resource_hash: str) -> FileResponse: """ - ResourceManagerから発行されたURLへのアクセスに対応する + ResourceManagerから発行されたハッシュ値に対応するリソースファイルを返す """ resource_path = resource_manager.resource_path(resource_name) if resource_path is None or not resource_path.exists(): From bc20984a70fd60dbe386944f9788ce668bf7e37b Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:57:45 +0900 Subject: [PATCH 50/82] Update voicevox_engine/resource_manager.py Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 9613c158c..7daee4acc 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -51,8 +51,7 @@ def register_dir(self, resource_dir: Path) -> None: def resource_str( self, resource_path: Path, - base_url: str, - resource_format: Literal["base64", "url"], + resource_format: Literal["base64", "hash"], ) -> str: filehash = self._path_to_hash.get(resource_path) if filehash is None: From dc1dc2266846ab39b79178ab63e6e82efe29dcec Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:54:14 +0900 Subject: [PATCH 51/82] FIX: suggestion --- voicevox_engine/app/routers/speaker.py | 12 +++++++----- voicevox_engine/metas/Metas.py | 3 ++- voicevox_engine/resource_manager.py | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 40f026070..a229d8328 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -109,9 +109,11 @@ def _speaker_info( policy = policy_path.read_text("utf-8") def _resource_str(path: Path) -> str: - return resource_manager.resource_str( - path, resource_baseurl, resource_format - ) + res_format = "hash" if resource_format == "url" else "base64" + resource_str = resource_manager.resource_str(path, res_format) + if resource_format == "base64": + return resource_str + return f"{resource_baseurl}/{resource_str}" # speaker portrait portrait_path = speaker_path / "portrait.png" @@ -188,12 +190,12 @@ async def resources(resource_hash: str) -> FileResponse: """ ResourceManagerから発行されたハッシュ値に対応するリソースファイルを返す """ - resource_path = resource_manager.resource_path(resource_name) + resource_path = resource_manager.resource_path(resource_hash) if resource_path is None or not resource_path.exists(): raise HTTPException(status_code=404) return FileResponse( resource_path, - headers={"Cache-Control": "max-age=2592000"}, # 30日 + headers={"Cache-Control": "max-age=2592000"}, # 30日 ) return router diff --git a/voicevox_engine/metas/Metas.py b/voicevox_engine/metas/Metas.py index c702493dc..73931d396 100644 --- a/voicevox_engine/metas/Metas.py +++ b/voicevox_engine/metas/Metas.py @@ -75,7 +75,8 @@ class StyleInfo(BaseModel): id: StyleId = Field(title="スタイルID") icon: str = Field(title="当該スタイルのアイコンのリソース") portrait: str | SkipJsonSchema[None] = Field( - default=None, title="このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL" + default=None, + title="このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL", ) voice_samples: list[str] = Field(title="サンプル音声のリソース") diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 7daee4acc..541c8994e 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -59,7 +59,7 @@ def resource_str( if resource_format == "base64": return b64encode_str(resource_path.read_bytes()) - return f"{base_url}/{filehash}" + return filehash def resource_path(self, filehash: str) -> Path | None: """指定したハッシュ値を持つリソースファイルのパスを返す。""" From 710fc154b189c69794a494e2d166aa27595f5a97 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:10:09 +0900 Subject: [PATCH 52/82] TST: fix test --- ...\343\202\222\347\242\272\350\252\215.json" | 4 +- test/e2e/__snapshots__/test_speakers.ambr | 72 ++++++++--------- test/e2e/test_speakers.py | 76 +++++++++-------- .../resource_manager/test_resource_manager.py | 81 +++++++------------ voicevox_engine/app/routers/speaker.py | 7 +- 5 files changed, 114 insertions(+), 126 deletions(-) diff --git "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" index d0b6025bb..85e85f024 100644 --- "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" +++ "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" @@ -728,7 +728,7 @@ "type": "string" }, "portrait": { - "title": "ポートレート画像のリソース", + "title": "立ち絵画像のリソース", "type": "string" }, "style_infos": { @@ -814,7 +814,7 @@ "type": "integer" }, "portrait": { - "title": "当該スタイルのポートレート画像のリソース", + "title": "このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL", "type": "string" }, "voice_samples": { diff --git a/test/e2e/__snapshots__/test_speakers.ambr b/test/e2e/__snapshots__/test_speakers.ambr index 20a4eeb41..b7ded7471 100644 --- a/test/e2e/__snapshots__/test_speakers.ambr +++ b/test/e2e/__snapshots__/test_speakers.ambr @@ -3,25 +3,25 @@ 'MD5:517ba089e0b03f8868af2ce956f7699d' # --- # name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_5_voice_sample_0] - 'MD5:517ba089e0b03f8868af2ce956f7699d' + 'MD5:d368ea4f7af3fba9f9f7fa862e50590f' # --- # name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_5_voice_sample_1] - 'MD5:517ba089e0b03f8868af2ce956f7699d' + 'MD5:a844eb96e25efd52dfee76d023eda0c8' # --- # name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_5_voice_sample_2] - 'MD5:517ba089e0b03f8868af2ce956f7699d' + 'MD5:4cd9a6ff5ac76ea1c267c99b2cfe925f' # --- # name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_7_icon] 'MD5:562ad0f61ca6dd81e89a4479f97dcd9f' # --- # name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_7_voice_sample_0] - 'MD5:562ad0f61ca6dd81e89a4479f97dcd9f' + 'MD5:f214d07c3fb5332e429abb17921eecb5' # --- # name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_7_voice_sample_1] - 'MD5:562ad0f61ca6dd81e89a4479f97dcd9f' + 'MD5:46296628b586d968054cc43ad733dd4a' # --- # name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_7_voice_sample_2] - 'MD5:562ad0f61ca6dd81e89a4479f97dcd9f' + 'MD5:919e8e440b31b13fb0d2aae03061661f' # --- # name: test_歌手の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_portrait] 'MD5:27777cb0883c98cd9870707005bf1faf' @@ -30,31 +30,31 @@ 'MD5:c6a8ddea789d8115372db31b4a76d2aa' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_portrait] - 'MD5:c6a8ddea789d8115372db31b4a76d2aa' + 'MD5:2b339e97d7ae0b6b215de842ff12515e' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_voice_sample_0] - 'MD5:c6a8ddea789d8115372db31b4a76d2aa' + 'MD5:5333edce35b3806f1818c407cd74b66b' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_voice_sample_1] - 'MD5:c6a8ddea789d8115372db31b4a76d2aa' + 'MD5:720ab16616e8e3e8e15535c9f6fd1f03' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_4_voice_sample_2] - 'MD5:c6a8ddea789d8115372db31b4a76d2aa' + 'MD5:444dd9f019828f3773192cca7d1099df' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_icon] 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_portrait] - 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' + 'MD5:6f7cd8ecb9d2d8de0ed410e05350ac72' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_voice_sample_0] - 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' + 'MD5:4add3a1cf25c4b6ce6d35aaa1f760071' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_voice_sample_1] - 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' + 'MD5:42d20380d8d6fc95916037e4da990aaa' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_6_voice_sample_2] - 'MD5:d2c7c85a9919372ef62ad49786bd7fc4' + 'MD5:af22bcd03f4959dc8f0e4ac1d04854e0' # --- # name: test_歌手の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_portrait] 'MD5:78228648fe527ee23597b66db59a4f94' @@ -63,13 +63,13 @@ 'MD5:1e3d0a6e88264b1b99839aeb7c7cbe1f' # --- # name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_9_voice_sample_0] - 'MD5:1e3d0a6e88264b1b99839aeb7c7cbe1f' + 'MD5:e8626456ba706690676639e61ad43b73' # --- # name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_9_voice_sample_1] - 'MD5:1e3d0a6e88264b1b99839aeb7c7cbe1f' + 'MD5:93e3d5c44bbad3ddfc43953f5c751670' # --- # name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_9_voice_sample_2] - 'MD5:1e3d0a6e88264b1b99839aeb7c7cbe1f' + 'MD5:765ff637563e64182ace61fb5ca7daf4' # --- # name: test_歌手の情報をURLで取得できる[b1a81618-b27b-40d2-b0ea-27a9ad408c4b_portrait] 'MD5:381ba07cbcad95f1c99109f5f6d096a8' @@ -78,16 +78,16 @@ 'MD5:1fe576e75458c752cfeecc1e93a29886' # --- # name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_portrait] - 'MD5:1fe576e75458c752cfeecc1e93a29886' + 'MD5:56b2cfc4a9118c40a999e151c0ac647e' # --- # name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_voice_sample_0] - 'MD5:1fe576e75458c752cfeecc1e93a29886' + 'MD5:62cce06e564276499df6014e7182368d' # --- # name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_voice_sample_1] - 'MD5:1fe576e75458c752cfeecc1e93a29886' + 'MD5:2164af6fc692d5b2117dfd845c880f81' # --- # name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_8_voice_sample_2] - 'MD5:1fe576e75458c752cfeecc1e93a29886' + 'MD5:4e81220a91745cf2ab7b632cd528ffbd' # --- # name: test_話者の情報をURLで取得できる[35b2c544-660e-401e-b503-0e14c635303a_portrait] 'MD5:bd6cf66dcc652f56892b14b423f6f37c' @@ -96,28 +96,28 @@ 'MD5:becb1cc2aaf82623a13a1250a39d7393' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_1_voice_sample_0] - 'MD5:becb1cc2aaf82623a13a1250a39d7393' + 'MD5:a9bf75355816d858213cb116942fe499' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_1_voice_sample_1] - 'MD5:becb1cc2aaf82623a13a1250a39d7393' + 'MD5:0dc81612c1f305b6210ef325a4518e53' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_1_voice_sample_2] - 'MD5:becb1cc2aaf82623a13a1250a39d7393' + 'MD5:ba5694044d8e7e0bffa9578d22ba2ba8' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_icon] 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_portrait] - 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' + 'MD5:0308b1a8e7a849e8be5ea699706f5097' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_voice_sample_0] - 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' + 'MD5:0fd8a039030ea31560c84e91f955e4cd' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_voice_sample_1] - 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' + 'MD5:c249264fa985fd4ab5e940c7e813db3e' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_3_voice_sample_2] - 'MD5:9a3690c368cd9a4ecb1940ff9eb2c955' + 'MD5:6a0bc8b54543fe816f37cb286795ad07' # --- # name: test_話者の情報をURLで取得できる[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9_portrait] 'MD5:27777cb0883c98cd9870707005bf1faf' @@ -126,31 +126,31 @@ 'MD5:1f1da5f25968c638a783bf6ba9df9420' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_portrait] - 'MD5:1f1da5f25968c638a783bf6ba9df9420' + 'MD5:1724b3741e58978f5b9db25eb6575d9b' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_voice_sample_0] - 'MD5:1f1da5f25968c638a783bf6ba9df9420' + 'MD5:79d52a44a8dc8548616c300e49b37a94' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_voice_sample_1] - 'MD5:1f1da5f25968c638a783bf6ba9df9420' + 'MD5:d31e59cf938a46d156add0da486fe5e2' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_0_voice_sample_2] - 'MD5:1f1da5f25968c638a783bf6ba9df9420' + 'MD5:73d7e1f311263ab7e1b468318cb8575a' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_icon] 'MD5:0b158046338f60a53e9afdb7797c5864' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_portrait] - 'MD5:0b158046338f60a53e9afdb7797c5864' + 'MD5:593c75329b6531e5ae0266a708e5ebc0' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_voice_sample_0] - 'MD5:0b158046338f60a53e9afdb7797c5864' + 'MD5:8e917c07fe5d444eddd4b451b7dc8d83' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_voice_sample_1] - 'MD5:0b158046338f60a53e9afdb7797c5864' + 'MD5:0cf95e313bc5c3314be6a71e5021fad2' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_2_voice_sample_2] - 'MD5:0b158046338f60a53e9afdb7797c5864' + 'MD5:43f9f43e4607015e7ab3bd016bee5509' # --- # name: test_話者の情報をURLで取得できる[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff_portrait] 'MD5:78228648fe527ee23597b66db59a4f94' diff --git a/test/e2e/test_speakers.py b/test/e2e/test_speakers.py index bfbfeca5b..1fd582982 100644 --- a/test/e2e/test_speakers.py +++ b/test/e2e/test_speakers.py @@ -20,6 +20,17 @@ def _hash_bytes(value: bytes) -> str: return "MD5:" + hashlib.md5(value).hexdigest() +def _assert_resource( + client: TestClient, snapshot: SnapshotAssertion, url: str, name: str +) -> None: + """ + URLからデータが正しく取得できるかスナップショットテストをする + """ + response = client.get(url) + assert response.status_code == 200 + assert snapshot(name=name) == _hash_bytes(response.content) + + def test_話者一覧が取得できる( client: TestClient, snapshot_json: SnapshotAssertion ) -> None: @@ -54,28 +65,28 @@ def test_話者の情報をURLで取得できる( assert snapshot_json(name=speaker_id) == response.json() speaker_info = SpeakerInfo.model_validate_json(response.content) - portrait = client.get(speaker_info.portrait) - assert portrait.status_code == 200 - assert snapshot(name=f"{speaker_id}_portrait") == _hash_bytes(portrait.content) + _assert_resource( + client, snapshot, speaker_info.portrait, f"{speaker_id}_portrait" + ) for style in speaker_info.style_infos: - icon = client.get(style.icon) - assert icon.status_code == 200 - assert snapshot(name=f"{speaker_id}_{style.id}_icon") == _hash_bytes( - icon.content + _assert_resource( + client, snapshot, style.icon, f"{speaker_id}_{style.id}_icon" ) if style.portrait is not None: - portrait = client.get(style.portrait) - assert portrait.status_code == 200 - assert snapshot( - name=f"{speaker_id}_{style.id}_portrait" - ) == _hash_bytes(icon.content) + _assert_resource( + client, + snapshot, + style.portrait, + f"{speaker_id}_{style.id}_portrait", + ) for i, voice_sample in enumerate(style.voice_samples): - sample = client.get(voice_sample) - assert sample.status_code == 200 - assert snapshot( - name=f"{speaker_id}_{style.id}_voice_sample_{i}" - ) == _hash_bytes(icon.content) + _assert_resource( + client, + snapshot, + voice_sample, + f"{speaker_id}_{style.id}_voice_sample_{i}", + ) def test_歌手一覧が取得できる( @@ -110,25 +121,24 @@ def test_歌手の情報をURLで取得できる( params={"speaker_uuid": singer_id, "resource_format": "url"}, ) assert snapshot_json(name=singer_id) == response.json() + speaker_info = SpeakerInfo.model_validate_json(response.content) - portrait = client.get(speaker_info.portrait) - assert portrait.status_code == 200 - assert snapshot(name=f"{singer_id}_portrait") == _hash_bytes(portrait.content) + _assert_resource( + client, snapshot, speaker_info.portrait, f"{singer_id}_portrait" + ) + for style in speaker_info.style_infos: - icon = client.get(style.icon) - assert icon.status_code == 200 - assert snapshot(name=f"{singer_id}_{style.id}_icon") == _hash_bytes( - icon.content + _assert_resource( + client, snapshot, style.icon, f"{singer_id}_{style.id}_icon" ) if style.portrait is not None: - portrait = client.get(style.portrait) - assert portrait.status_code == 200 - assert snapshot(name=f"{singer_id}_{style.id}_portrait") == _hash_bytes( - icon.content + _assert_resource( + client, snapshot, style.portrait, f"{singer_id}_{style.id}_portrait" ) for i, voice_sample in enumerate(style.voice_samples): - sample = client.get(voice_sample) - assert sample.status_code == 200 - assert snapshot( - name=f"{singer_id}_{style.id}_voice_sample_{i}" - ) == _hash_bytes(icon.content) + _assert_resource( + client, + snapshot, + voice_sample, + f"{singer_id}_{style.id}_voice_sample_{i}", + ) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index d0099f825..44f87bb0b 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -1,5 +1,4 @@ import base64 -from os.path import basename from pathlib import Path import pytest @@ -9,48 +8,48 @@ with_filemap_dir = Path(__file__).parent / "with_filemap" without_filemap_dir = Path(__file__).parent / "without_filemap" -dummy_base_url = "http://localhost" - def b64encode_str(s: bytes) -> str: return base64.b64encode(s).decode("utf-8") +def _assert_resource(manager: ResourceManager, test_path: Path) -> None: + """ + `test_path`で指定したファイルが正しくbase64とハッシュが取得できるか確認する + また、取得したハッシュから取得したファイルから同じバイト列が取得できるか確認する + """ + test_bytes = test_path.read_bytes() + + assert manager.resource_str(test_path, "base64") == b64encode_str(test_bytes) + + test_filehash = manager.resource_str(test_path, "hash") + test_res_path = manager.resource_path(test_filehash) + assert test_res_path is not None + assert test_res_path.read_bytes() == test_bytes + + def test_with_filemap() -> None: manager = ResourceManager(False) manager.register_dir(with_filemap_dir) - png_path = with_filemap_dir / "dummy.png" - png_bytes = png_path.read_bytes() - assert manager.resource_str(png_path, dummy_base_url, "base64") == b64encode_str( - png_bytes - ) - png_filehash = basename(manager.resource_str(png_path, dummy_base_url, "url")) - png_res_path = manager.resource_path(png_filehash) - assert png_res_path is not None - assert png_res_path.read_bytes() == png_bytes + png_path = with_filemap_dir / "dummy.png" + _assert_resource(manager, png_path) wav_path = with_filemap_dir / "dummy.wav" - wav_bytes = wav_path.read_bytes() - - assert manager.resource_str(wav_path, dummy_base_url, "base64") == b64encode_str( - wav_bytes - ) - wav_filehash = basename(manager.resource_str(wav_path, dummy_base_url, "url")) - wav_res_path = manager.resource_path(wav_filehash) - assert wav_res_path is not None - assert wav_res_path.read_bytes() == wav_bytes + _assert_resource(manager, wav_path) + # テキストは通常エンコードせずにJSONに含めるためResourceManagerでは管理しない txt_path = with_filemap_dir / "dummy.txt" with pytest.raises(ResourceManagerError) as _: - manager.resource_str(txt_path, dummy_base_url, "base64") + manager.resource_str(txt_path, "base64") with pytest.raises(ResourceManagerError) as _: - manager.resource_str(txt_path, dummy_base_url, "url") + manager.resource_str(txt_path, "hash") - assert manager.resource_path("BAD_HASH") is None + assert manager.resource_path("NOT_EXIST_HASH") is None def test_without_filemap_when_production() -> None: + # "create_filemap_if_not_exist"がFaseで"filemap.json"が無い場合エラーにする manager = ResourceManager(False) with pytest.raises(ResourceManagerError) as _: manager.register_dir(without_filemap_dir) @@ -59,37 +58,15 @@ def test_without_filemap_when_production() -> None: def test_without_filemap() -> None: manager = ResourceManager(True) manager.register_dir(without_filemap_dir) - png_path = without_filemap_dir / "dummy.png" - png_bytes = png_path.read_bytes() - assert manager.resource_str(png_path, dummy_base_url, "base64") == b64encode_str( - png_bytes - ) - png_filehash = basename(manager.resource_str(png_path, dummy_base_url, "url")) - png_res_path = manager.resource_path(png_filehash) - assert png_res_path is not None - assert png_res_path.read_bytes() == png_bytes + png_path = without_filemap_dir / "dummy.png" + _assert_resource(manager, png_path) wav_path = without_filemap_dir / "dummy.wav" - wav_bytes = wav_path.read_bytes() - - assert manager.resource_str(wav_path, dummy_base_url, "base64") == b64encode_str( - wav_bytes - ) - wav_filehash = basename(manager.resource_str(wav_path, dummy_base_url, "url")) - wav_res_path = manager.resource_path(wav_filehash) - assert wav_res_path is not None - assert wav_res_path.read_bytes() == wav_bytes + _assert_resource(manager, wav_path) + # "filemap.json"がない場合、全てのファイルが公開される txt_path = without_filemap_dir / "dummy.txt" - txt_bytes = txt_path.read_bytes() - - assert manager.resource_str(txt_path, dummy_base_url, "base64") == b64encode_str( - txt_bytes - ) - txt_filehash = basename(manager.resource_str(txt_path, dummy_base_url, "url")) - txt_res_path = manager.resource_path(txt_filehash) - assert txt_res_path is not None - assert txt_res_path.read_bytes() == txt_bytes + _assert_resource(manager, txt_path) - assert manager.resource_path("BAD_HASH") is None + assert manager.resource_path("NOT_EXIST_HASH") is None diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index a229d8328..6c80f43de 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -109,8 +109,9 @@ def _speaker_info( policy = policy_path.read_text("utf-8") def _resource_str(path: Path) -> str: - res_format = "hash" if resource_format == "url" else "base64" - resource_str = resource_manager.resource_str(path, res_format) + resource_str = resource_manager.resource_str( + path, "hash" if resource_format == "url" else "base64" + ) if resource_format == "base64": return resource_str return f"{resource_baseurl}/{resource_str}" @@ -185,7 +186,7 @@ def singer_info( ) # リソースはAPIとしてアクセスするものではないことを表明するためOpenAPIスキーマーから除外する - @router.get(f"/{RESOURCE_ENDPOINT}/{{resource_name}}", include_in_schema=False) + @router.get(f"/{RESOURCE_ENDPOINT}/{{resource_hash}}", include_in_schema=False) async def resources(resource_hash: str) -> FileResponse: """ ResourceManagerから発行されたハッシュ値に対応するリソースファイルを返す From 91df0d34235190d355929358b10bfbf1ac628970 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:23:00 +0900 Subject: [PATCH 53/82] FIX: typo --- test/unit/resource_manager/test_resource_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index 44f87bb0b..6b01cf5c8 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -49,7 +49,7 @@ def test_with_filemap() -> None: def test_without_filemap_when_production() -> None: - # "create_filemap_if_not_exist"がFaseで"filemap.json"が無い場合エラーにする + # "create_filemap_if_not_exist"がFalseで"filemap.json"が無い場合エラーにする manager = ResourceManager(False) with pytest.raises(ResourceManagerError) as _: manager.register_dir(without_filemap_dir) From 2e034b82528e392692720d7e9669b56a024a24c9 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:06:36 +0900 Subject: [PATCH 54/82] =?UTF-8?q?FIX:=20`ResourceManager`=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=82=BF=E3=83=B3=E3=82=B9=E5=8C=96=E3=81=AE?= =?UTF-8?q?=E5=A0=B4=E6=89=80=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voicevox_engine/app/application.py b/voicevox_engine/app/application.py index 7c6f1fb98..ada68a2c1 100644 --- a/voicevox_engine/app/application.py +++ b/voicevox_engine/app/application.py @@ -64,6 +64,8 @@ def generate_app( deprecated_mutable_api.enable = False metas_store = MetasStore(speaker_info_dir) + resource_manager = ResourceManager(is_development()) + resource_manager.register_dir(speaker_info_dir) app.include_router( generate_tts_pipeline_router( @@ -73,8 +75,6 @@ def generate_app( app.include_router(generate_morphing_router(tts_engines, core_manager, metas_store)) app.include_router(generate_preset_router(preset_manager)) - resource_manager = ResourceManager(is_development()) - resource_manager.register_dir(speaker_info_dir) app.include_router( generate_speaker_router( core_manager, resource_manager, metas_store, speaker_info_dir From 3fb026c64229544acabeb9620d9bf53467f2d289 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:10:31 +0900 Subject: [PATCH 55/82] FIX: remane test function --- test/e2e/test_speakers.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/e2e/test_speakers.py b/test/e2e/test_speakers.py index 1fd582982..a4af0ac22 100644 --- a/test/e2e/test_speakers.py +++ b/test/e2e/test_speakers.py @@ -20,7 +20,7 @@ def _hash_bytes(value: bytes) -> str: return "MD5:" + hashlib.md5(value).hexdigest() -def _assert_resource( +def _assert_resource_url( client: TestClient, snapshot: SnapshotAssertion, url: str, name: str ) -> None: """ @@ -65,23 +65,23 @@ def test_話者の情報をURLで取得できる( assert snapshot_json(name=speaker_id) == response.json() speaker_info = SpeakerInfo.model_validate_json(response.content) - _assert_resource( + _assert_resource_url( client, snapshot, speaker_info.portrait, f"{speaker_id}_portrait" ) for style in speaker_info.style_infos: - _assert_resource( + _assert_resource_url( client, snapshot, style.icon, f"{speaker_id}_{style.id}_icon" ) if style.portrait is not None: - _assert_resource( + _assert_resource_url( client, snapshot, style.portrait, f"{speaker_id}_{style.id}_portrait", ) for i, voice_sample in enumerate(style.voice_samples): - _assert_resource( + _assert_resource_url( client, snapshot, voice_sample, @@ -123,20 +123,20 @@ def test_歌手の情報をURLで取得できる( assert snapshot_json(name=singer_id) == response.json() speaker_info = SpeakerInfo.model_validate_json(response.content) - _assert_resource( + _assert_resource_url( client, snapshot, speaker_info.portrait, f"{singer_id}_portrait" ) for style in speaker_info.style_infos: - _assert_resource( + _assert_resource_url( client, snapshot, style.icon, f"{singer_id}_{style.id}_icon" ) if style.portrait is not None: - _assert_resource( + _assert_resource_url( client, snapshot, style.portrait, f"{singer_id}_{style.id}_portrait" ) for i, voice_sample in enumerate(style.voice_samples): - _assert_resource( + _assert_resource_url( client, snapshot, voice_sample, From b8d3558d2b8779c355c35cf7c0fe7ce8ce9b388d Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:45:23 +0900 Subject: [PATCH 56/82] =?UTF-8?q?FIX:=20=E3=82=B3=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=88=E8=BF=BD=E5=8A=A0=E3=83=BB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/resource_manager/test_resource_manager.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index 6b01cf5c8..e21027737 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -29,6 +29,11 @@ def _assert_resource(manager: ResourceManager, test_path: Path) -> None: def test_with_filemap() -> None: + """ + "filemap.json"があるディレクトリでのテスト + fimemapの生成方法 + `python build_util/generate_filemap.py --target_dir test/unit/resource_manager/with_filemap` + """ manager = ResourceManager(False) manager.register_dir(with_filemap_dir) @@ -45,17 +50,23 @@ def test_with_filemap() -> None: with pytest.raises(ResourceManagerError) as _: manager.resource_str(txt_path, "hash") + # 登録されていないハッシュが渡された場合Noneを返す assert manager.resource_path("NOT_EXIST_HASH") is None def test_without_filemap_when_production() -> None: - # "create_filemap_if_not_exist"がFalseで"filemap.json"が無い場合エラーにする + """ + "create_filemap_if_not_exist"がFalseで"filemap.json"が無い場合エラーにする(製品版を想定) + """ manager = ResourceManager(False) with pytest.raises(ResourceManagerError) as _: manager.register_dir(without_filemap_dir) def test_without_filemap() -> None: + """ + "create_filemap_if_not_exist"がTrueで"filemap.json"が無い場合エラーにする(開発時を想定) + """ manager = ResourceManager(True) manager.register_dir(without_filemap_dir) @@ -69,4 +80,5 @@ def test_without_filemap() -> None: txt_path = without_filemap_dir / "dummy.txt" _assert_resource(manager, txt_path) + # 登録されていないハッシュが渡された場合Noneを返す assert manager.resource_path("NOT_EXIST_HASH") is None From 1503b18339f7faaeacdd7aced7077ffe2f3d471e Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:31:30 +0900 Subject: [PATCH 57/82] =?UTF-8?q?TST:=20=E5=90=8C=E3=81=98=E3=83=90?= =?UTF-8?q?=E3=82=A4=E3=83=8A=E3=83=AA=E3=81=8C=E3=81=82=E3=82=8B=E5=A0=B4?= =?UTF-8?q?=E5=90=88=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/resource_manager/test_resource_manager.py | 10 ++++++++++ .../with_filemap/dummy_same_binary.wav | Bin 0 -> 44 bytes .../resource_manager/with_filemap/filemap.json | 2 +- .../without_filemap/dummy_same_binary.wav | Bin 0 -> 44 bytes 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 test/unit/resource_manager/with_filemap/dummy_same_binary.wav create mode 100644 test/unit/resource_manager/without_filemap/dummy_same_binary.wav diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index e21027737..93551e1d4 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -43,6 +43,11 @@ def test_with_filemap() -> None: wav_path = with_filemap_dir / "dummy.wav" _assert_resource(manager, wav_path) + # 同じバイナリがある場合のテスト + same_wav_path = with_filemap_dir / "dummy_same_binary.wav" + assert wav_path.read_bytes() == same_wav_path.read_bytes() + _assert_resource(manager, same_wav_path) + # テキストは通常エンコードせずにJSONに含めるためResourceManagerでは管理しない txt_path = with_filemap_dir / "dummy.txt" with pytest.raises(ResourceManagerError) as _: @@ -76,6 +81,11 @@ def test_without_filemap() -> None: wav_path = without_filemap_dir / "dummy.wav" _assert_resource(manager, wav_path) + # 同じバイナリがある場合のテスト + same_wav_path = without_filemap_dir / "dummy_same_binary.wav" + assert wav_path.read_bytes() == same_wav_path.read_bytes() + _assert_resource(manager, same_wav_path) + # "filemap.json"がない場合、全てのファイルが公開される txt_path = without_filemap_dir / "dummy.txt" _assert_resource(manager, txt_path) diff --git a/test/unit/resource_manager/with_filemap/dummy_same_binary.wav b/test/unit/resource_manager/with_filemap/dummy_same_binary.wav new file mode 100644 index 0000000000000000000000000000000000000000..31cbbee871f1630dc549642a4154df22397a7648 GIT binary patch literal 44 vcmWIYbaPW-U| Date: Wed, 19 Jun 2024 00:13:27 +0900 Subject: [PATCH 58/82] =?UTF-8?q?FIX:=20=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E8=AA=AC=E6=98=8E=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1\250\343\202\222\347\242\272\350\252\215.json" | 10 +++++----- voicevox_engine/metas/Metas.py | 14 +++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" index 85e85f024..0b860d96c 100644 --- "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" +++ "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" @@ -721,14 +721,14 @@ "type": "object" }, "SpeakerInfo": { - "description": "話者の追加情報\n`リソース`はバイナリをbase64エンコードしたもの、または取得用のURL", + "description": "話者の追加情報\n画像や音声はバイナリをbase64エンコードしたもの、あるいはURL", "properties": { "policy": { "title": "policy.md", "type": "string" }, "portrait": { - "title": "立ち絵画像のリソース", + "title": "立ち絵画像をbase64エンコードしたもの、あるいはURL", "type": "string" }, "style_infos": { @@ -803,10 +803,10 @@ "type": "object" }, "StyleInfo": { - "description": "スタイルの追加情報\n`リソース`はバイナリをbase64エンコードしたもの、または取得用のURL", + "description": "スタイルの追加情報\n画像や音声はバイナリをbase64エンコードしたもの、あるいはURL", "properties": { "icon": { - "title": "当該スタイルのアイコンのリソース", + "title": "このスタイルのアイコンをbase64エンコードしたもの、あるいはURL", "type": "string" }, "id": { @@ -821,7 +821,7 @@ "items": { "type": "string" }, - "title": "サンプル音声のリソース", + "title": "サンプル音声をbase64エンコードしたもの、あるいはURL", "type": "array" } }, diff --git a/voicevox_engine/metas/Metas.py b/voicevox_engine/metas/Metas.py index 73931d396..77682e48b 100644 --- a/voicevox_engine/metas/Metas.py +++ b/voicevox_engine/metas/Metas.py @@ -69,24 +69,28 @@ class Speaker(BaseModel): class StyleInfo(BaseModel): """ スタイルの追加情報 - `リソース`はバイナリをbase64エンコードしたもの、または取得用のURL + 画像や音声はバイナリをbase64エンコードしたもの、あるいはURL """ id: StyleId = Field(title="スタイルID") - icon: str = Field(title="当該スタイルのアイコンのリソース") + icon: str = Field( + title="このスタイルのアイコンをbase64エンコードしたもの、あるいはURL" + ) portrait: str | SkipJsonSchema[None] = Field( default=None, title="このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL", ) - voice_samples: list[str] = Field(title="サンプル音声のリソース") + voice_samples: list[str] = Field( + title="サンプル音声をbase64エンコードしたもの、あるいはURL" + ) class SpeakerInfo(BaseModel): """ 話者の追加情報 - `リソース`はバイナリをbase64エンコードしたもの、または取得用のURL + 画像や音声はバイナリをbase64エンコードしたもの、あるいはURL """ policy: str = Field(title="policy.md") - portrait: str = Field(title="立ち絵画像のリソース") + portrait: str = Field(title="立ち絵画像をbase64エンコードしたもの、あるいはURL") style_infos: list[StyleInfo] = Field(title="スタイルの追加情報") From 4770f635690bb06dc17213a895a09a79581fa7ad Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:15:45 +0900 Subject: [PATCH 59/82] FIX: add comment Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 541c8994e..99b5d0340 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -53,6 +53,7 @@ def resource_str( resource_path: Path, resource_format: Literal["base64", "hash"], ) -> str: + # NOTE: 意図しないパスのファイルの結果を返さないようにする filehash = self._path_to_hash.get(resource_path) if filehash is None: raise ResourceManagerError(f"{resource_path}がfilemapに登録されていません") From 8e0e944ca21cbcad529380ea2354cfe15544fc2a Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:16:13 +0900 Subject: [PATCH 60/82] FIX: add comment Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 99b5d0340..b254cb660 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -54,6 +54,7 @@ def resource_str( resource_format: Literal["base64", "hash"], ) -> str: # NOTE: 意図しないパスのファイルの結果を返さないようにする + """指定したリソースファイルのbase64文字列やハッシュ値を返す。""" filehash = self._path_to_hash.get(resource_path) if filehash is None: raise ResourceManagerError(f"{resource_path}がfilemapに登録されていません") From e8941adecd0057a8628b9f52561dcfec33d29b8f Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:17:05 +0900 Subject: [PATCH 61/82] FIX: rename variable Co-authored-by: Hiroshiba --- build_util/generate_filemap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_util/generate_filemap.py b/build_util/generate_filemap.py index 563dae93c..604f6dd7f 100644 --- a/build_util/generate_filemap.py +++ b/build_util/generate_filemap.py @@ -9,7 +9,7 @@ from hashlib import sha256 from pathlib import Path, PurePosixPath -DEFAULT_FILENAME = "filemap.json" +FILEMAP_FILENAME = "filemap.json" DEFAULT_TARGET_SUFFIX = ["png", "wav"] From 33eef5e980bc3d0b9d35e622d91baa23561c8676 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:20:05 +0900 Subject: [PATCH 62/82] FIX: rename variable --- build_util/generate_filemap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_util/generate_filemap.py b/build_util/generate_filemap.py index 604f6dd7f..1065aca41 100644 --- a/build_util/generate_filemap.py +++ b/build_util/generate_filemap.py @@ -56,6 +56,6 @@ def generate_path_to_hash_dict( if not target_dir.is_dir(): raise Exception(f"{target_dir}はディレクトリではありません") - save_path = target_dir / DEFAULT_FILENAME + save_path = target_dir / FILEMAP_FILENAME path_to_hash = generate_path_to_hash_dict(target_dir, args.target_suffix) save_path.write_text(json.dumps(path_to_hash, ensure_ascii=False), encoding="utf-8") From 91736d243db9db5a7ba614e1e3813a3b36fb14ce Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:36:56 +0900 Subject: [PATCH 63/82] FIX: variable and description Co-authored-by: Hiroshiba --- .../resource_manager/test_resource_manager.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index 93551e1d4..f23d13909 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -13,19 +13,19 @@ def b64encode_str(s: bytes) -> str: return base64.b64encode(s).decode("utf-8") -def _assert_resource(manager: ResourceManager, test_path: Path) -> None: +def _assert_resource(manager: ResourceManager, input_path: Path) -> None: """ - `test_path`で指定したファイルが正しくbase64とハッシュが取得できるか確認する - また、取得したハッシュから取得したファイルから同じバイト列が取得できるか確認する + `test_path`で指定したファイルから正しくbase64が取得できるか確認する + また、ハッシュを取得し、対応するファイルから同じバイト列が取得できるか確認する """ - test_bytes = test_path.read_bytes() + true_bytes = input_path.read_bytes() - assert manager.resource_str(test_path, "base64") == b64encode_str(test_bytes) + assert manager.resource_str(input_path, "base64") == b64encode_str(true_bytes) - test_filehash = manager.resource_str(test_path, "hash") - test_res_path = manager.resource_path(test_filehash) - assert test_res_path is not None - assert test_res_path.read_bytes() == test_bytes + result_filehash = manager.resource_str(input_path, "hash") + result_path = manager.resource_path(result_filehash) + assert result_path is not None + assert result_path.read_bytes() == true_bytes def test_with_filemap() -> None: From ec7d89e659ad0e664950d7a9c43334b409c90405 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:37:41 +0900 Subject: [PATCH 64/82] FIX: rename --- test/unit/resource_manager/test_resource_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index f23d13909..b9983189f 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -15,7 +15,7 @@ def b64encode_str(s: bytes) -> str: def _assert_resource(manager: ResourceManager, input_path: Path) -> None: """ - `test_path`で指定したファイルから正しくbase64が取得できるか確認する + `input_path`で指定したファイルから正しくbase64が取得できるか確認する また、ハッシュを取得し、対応するファイルから同じバイト列が取得できるか確認する """ true_bytes = input_path.read_bytes() From 90d22efd58840cad1c7436208208c9764a7c858a Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:41:23 +0900 Subject: [PATCH 65/82] FIX: Comment Co-authored-by: Hiroshiba --- test/unit/resource_manager/test_resource_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index b9983189f..a2f0fbe7b 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -48,7 +48,8 @@ def test_with_filemap() -> None: assert wav_path.read_bytes() == same_wav_path.read_bytes() _assert_resource(manager, same_wav_path) - # テキストは通常エンコードせずにJSONに含めるためResourceManagerでは管理しない + # filemap.jsonに含まれないものはエラー + # NOTE: テキストは通常エンコードせずにJSONに含めるためResourceManagerでは管理しない txt_path = with_filemap_dir / "dummy.txt" with pytest.raises(ResourceManagerError) as _: manager.resource_str(txt_path, "base64") From a24a2b0d724e83b5471831af30d466bd74e271ba Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:18:56 +0900 Subject: [PATCH 66/82] =?UTF-8?q?FIX:=20=E5=AF=BE=E5=BF=9C=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=83=AA=E3=82=BD=E3=83=BC=E3=82=B9=E3=81=8C=E7=84=A1?= =?UTF-8?q?=E3=81=84=E5=A0=B4=E5=90=88=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92?= =?UTF-8?q?=E8=BF=94=E3=81=99=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource_manager/test_resource_manager.py | 17 +++++++++-------- voicevox_engine/app/routers/speaker.py | 5 +++-- voicevox_engine/resource_manager.py | 8 ++++++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index a2f0fbe7b..0df2b6f00 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -24,7 +24,6 @@ def _assert_resource(manager: ResourceManager, input_path: Path) -> None: result_filehash = manager.resource_str(input_path, "hash") result_path = manager.resource_path(result_filehash) - assert result_path is not None assert result_path.read_bytes() == true_bytes @@ -51,13 +50,14 @@ def test_with_filemap() -> None: # filemap.jsonに含まれないものはエラー # NOTE: テキストは通常エンコードせずにJSONに含めるためResourceManagerでは管理しない txt_path = with_filemap_dir / "dummy.txt" - with pytest.raises(ResourceManagerError) as _: + with pytest.raises(ResourceManagerError): manager.resource_str(txt_path, "base64") - with pytest.raises(ResourceManagerError) as _: + with pytest.raises(ResourceManagerError): manager.resource_str(txt_path, "hash") - # 登録されていないハッシュが渡された場合Noneを返す - assert manager.resource_path("NOT_EXIST_HASH") is None + # 登録されていないハッシュが渡された場合エラー + with pytest.raises(ResourceManagerError): + manager.resource_path("NOT_EXIST_HASH") def test_without_filemap_when_production() -> None: @@ -65,7 +65,7 @@ def test_without_filemap_when_production() -> None: "create_filemap_if_not_exist"がFalseで"filemap.json"が無い場合エラーにする(製品版を想定) """ manager = ResourceManager(False) - with pytest.raises(ResourceManagerError) as _: + with pytest.raises(ResourceManagerError): manager.register_dir(without_filemap_dir) @@ -91,5 +91,6 @@ def test_without_filemap() -> None: txt_path = without_filemap_dir / "dummy.txt" _assert_resource(manager, txt_path) - # 登録されていないハッシュが渡された場合Noneを返す - assert manager.resource_path("NOT_EXIST_HASH") is None + # 登録されていないハッシュが渡された場合エラー + with pytest.raises(ResourceManagerError): + manager.resource_path("NOT_EXIST_HASH") diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index 73bdec334..07613cdec 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -191,8 +191,9 @@ async def resources(resource_hash: str) -> FileResponse: """ ResourceManagerから発行されたハッシュ値に対応するリソースファイルを返す """ - resource_path = resource_manager.resource_path(resource_hash) - if resource_path is None or not resource_path.exists(): + try: + resource_path = resource_manager.resource_path(resource_hash) + except ResourceManagerError: raise HTTPException(status_code=404) return FileResponse( resource_path, diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index b254cb660..a72820a38 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -63,6 +63,10 @@ def resource_str( return b64encode_str(resource_path.read_bytes()) return filehash - def resource_path(self, filehash: str) -> Path | None: + def resource_path(self, filehash: str) -> Path: """指定したハッシュ値を持つリソースファイルのパスを返す。""" - return self._hash_to_path.get(filehash) + resource_path = self._hash_to_path.get(filehash) + + if resource_path is None: + raise ResourceManagerError(f"'{filehash}'に対応するリソースがありません") + return resource_path From a174ee6045976a464f012ffdc8c22071c20fb85a Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 20 Jun 2024 07:54:41 +0900 Subject: [PATCH 67/82] FIX: remove model description Co-authored-by: Hiroshiba --- voicevox_engine/metas/Metas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/voicevox_engine/metas/Metas.py b/voicevox_engine/metas/Metas.py index 77682e48b..ee5d83390 100644 --- a/voicevox_engine/metas/Metas.py +++ b/voicevox_engine/metas/Metas.py @@ -69,7 +69,6 @@ class Speaker(BaseModel): class StyleInfo(BaseModel): """ スタイルの追加情報 - 画像や音声はバイナリをbase64エンコードしたもの、あるいはURL """ id: StyleId = Field(title="スタイルID") From bd57f7e0fd625dce01118af57f87c8efac9ba38b Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 20 Jun 2024 07:55:07 +0900 Subject: [PATCH 68/82] FIX: remove model description Co-authored-by: Hiroshiba --- voicevox_engine/metas/Metas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/voicevox_engine/metas/Metas.py b/voicevox_engine/metas/Metas.py index ee5d83390..a5c11daa9 100644 --- a/voicevox_engine/metas/Metas.py +++ b/voicevox_engine/metas/Metas.py @@ -87,7 +87,6 @@ class StyleInfo(BaseModel): class SpeakerInfo(BaseModel): """ 話者の追加情報 - 画像や音声はバイナリをbase64エンコードしたもの、あるいはURL """ policy: str = Field(title="policy.md") From 28c4cdb0ab9035d115b60b3331757c14e11a26d5 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 20 Jun 2024 08:03:07 +0900 Subject: [PATCH 69/82] TST: Update snapshot --- ...\223\343\201\250\343\202\222\347\242\272\350\252\215.json" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" index 0b860d96c..11eee4366 100644 --- "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" +++ "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" @@ -721,7 +721,7 @@ "type": "object" }, "SpeakerInfo": { - "description": "話者の追加情報\n画像や音声はバイナリをbase64エンコードしたもの、あるいはURL", + "description": "話者の追加情報", "properties": { "policy": { "title": "policy.md", @@ -803,7 +803,7 @@ "type": "object" }, "StyleInfo": { - "description": "スタイルの追加情報\n画像や音声はバイナリをbase64エンコードしたもの、あるいはURL", + "description": "スタイルの追加情報", "properties": { "icon": { "title": "このスタイルのアイコンをbase64エンコードしたもの、あるいはURL", From 87e49f7e18552f56d464afc7913bffc2bec312b9 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:43:04 +0900 Subject: [PATCH 70/82] FIX: fix comments --- test/unit/resource_manager/test_resource_manager.py | 12 ++++++------ voicevox_engine/resource_manager.py | 6 ++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index 0df2b6f00..b8fbbf699 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -62,7 +62,7 @@ def test_with_filemap() -> None: def test_without_filemap_when_production() -> None: """ - "create_filemap_if_not_exist"がFalseで"filemap.json"が無い場合エラーにする(製品版を想定) + "create_filemap_if_not_exist"がFalseで"filemap.json"が無い場合エラーにする """ manager = ResourceManager(False) with pytest.raises(ResourceManagerError): @@ -71,26 +71,26 @@ def test_without_filemap_when_production() -> None: def test_without_filemap() -> None: """ - "create_filemap_if_not_exist"がTrueで"filemap.json"が無い場合エラーにする(開発時を想定) + "create_filemap_if_not_exist"がTrueで"filemap.json"が無い場合は登録時にfilemapを生成する """ manager = ResourceManager(True) manager.register_dir(without_filemap_dir) + # 全てのファイルが管理される png_path = without_filemap_dir / "dummy.png" _assert_resource(manager, png_path) wav_path = without_filemap_dir / "dummy.wav" _assert_resource(manager, wav_path) + txt_path = without_filemap_dir / "dummy.txt" + _assert_resource(manager, txt_path) + # 同じバイナリがある場合のテスト same_wav_path = without_filemap_dir / "dummy_same_binary.wav" assert wav_path.read_bytes() == same_wav_path.read_bytes() _assert_resource(manager, same_wav_path) - # "filemap.json"がない場合、全てのファイルが公開される - txt_path = without_filemap_dir / "dummy.txt" - _assert_resource(manager, txt_path) - # 登録されていないハッシュが渡された場合エラー with pytest.raises(ResourceManagerError): manager.resource_path("NOT_EXIST_HASH") diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index a72820a38..528265e56 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -27,6 +27,12 @@ class ResourceManager: """ def __init__(self, create_filemap_if_not_exist: bool) -> None: + """ + Parameters + ---------- + create_filemap_if_not_exist : bool + `filemap.json`がない場合でも登録時にfilemapを生成するか(開発時を想定) + """ self._create_filemap_if_not_exist = create_filemap_if_not_exist self._path_to_hash: dict[Path, str] = {} self._hash_to_path: dict[str, Path] = {} From 5e2c5210dc2b1f46cfd70696651483987c9ef2be Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 21 Jun 2024 00:13:31 +0900 Subject: [PATCH 71/82] =?UTF-8?q?DOC:=20`ResourceManager`=E3=81=AE?= =?UTF-8?q?=E4=BB=95=E6=A7=98=E3=81=AB=E3=81=A4=E3=81=84=E3=81=A6=E3=81=AE?= =?UTF-8?q?=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_util/generate_filemap.py | 1 + ...53\343\201\244\343\201\204\343\201\246.md" | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 "docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" diff --git a/build_util/generate_filemap.py b/build_util/generate_filemap.py index 1065aca41..47f002d70 100644 --- a/build_util/generate_filemap.py +++ b/build_util/generate_filemap.py @@ -13,6 +13,7 @@ DEFAULT_TARGET_SUFFIX = ["png", "wav"] +# WindowsとPOSIXで同じファイルが生成されるようにPurePosixPathに変換してから文字列にする。 def to_posix_str_path(path: Path) -> str: return str(PurePosixPath(path)) diff --git "a/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" "b/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" new file mode 100644 index 000000000..33cc4bf3f --- /dev/null +++ "b/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" @@ -0,0 +1,79 @@ +# ResourceManagerの仕様について + +## ResourceManager + +### 初期化 + +`create_filemap_if_not_exist`に`True`を渡すことによって登録するディレクトリに`filemap.json`が存在しないディレクトリを登録可能にします。 +これは開発時を想定したものです。 + +### リソースの登録 + +`register_dir(resource_dir)`でディレクトリ内のリソースを登録します。 +登録されるリソースは`filemap.json`にあるものに限ります。 + +`filemap.json`がない場合 +- `create_filemap_if_not_exist`が`False` +`ResourceManagerError`が発生します。 + +- `create_filemap_if_not_exist`が`True` +ディレクトリ内のすべてのファイルが登録されます。 + +## filemap.json + +### 仕様 + +キーは登録するファイルパスを登録するディレクトリを基準にした相対パスです。 + +値は登録するファイルを一意に識別できるハッシュ等の文字列です。 +`generate_filemap.py`はsha256ハッシュを生成します。 + +### 例 + +#### デイレクトリ構造 + +``` +├── ... +├── resources/ +│ └── 登録ディレクトリ/ +│ ├── filemap.json +│ ├── dir_1/ +│ │ ├── 登録ファイル.png +│ │ ├── samples/ +│ │ │ └── 登録ファイル.wav +│ │ └── 非登録ファイル1.txt +│ └── dir_2/ +│ ├── 登録ファイル.png +│ ├── samples/ +│ │ └── 登録ファイル.wav +│ └── 非登録ファイル1.txt +├── ... +``` + +#### filemap.json + +```json +{ + "dir_1/登録ファイル.png": "HASH-1", + "dir_1/samples/登録ファイル.wav": "HASH-2", + "dir_2/登録ファイル.png": "HASH-3", + "dir_2/samples/登録ファイル.wav": "HASH-4", +} +``` + +## generate_filemap.py + +`filemap.json`を生成するためのスクリプトです。 +デフォルトではpngファイルとwavファイルのみを登録します。 + +### 例 + +```bash +python build_util/generate_filemap.py --target_dir resources/character_info +``` + +pngとwavに加えてjpgファイルを登録する例 +```bash +python build_util/generate_filemap.py --target_dir resources/character_info \ + --target_suffix png --target_suffix wav --target_suffix jpg +``` From 0a40b56a194bff94463fc8c611062c1d0a27f98a Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 21 Jun 2024 07:34:01 +0900 Subject: [PATCH 72/82] =?UTF-8?q?FIX:=20=E4=BE=8B=E3=81=AE=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=88=E3=83=AA=E3=83=84=E3=83=AA?= =?UTF-8?q?=E3=83=BC=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...53\343\201\244\343\201\204\343\201\246.md" | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git "a/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" "b/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" index 900d63d8d..6d7baa3c7 100644 --- "a/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" +++ "b/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" @@ -33,21 +33,18 @@ #### デイレクトリ構造 ``` -├── ... -├── resources/ -│ └── 登録ディレクトリ/ -│ ├── filemap.json -│ ├── dir_1/ -│ │ ├── 登録ファイル.png -│ │ ├── samples/ -│ │ │ └── 登録ファイル.wav -│ │ └── 非登録ファイル1.txt -│ └── dir_2/ -│ ├── 登録ファイル.png -│ ├── samples/ -│ │ └── 登録ファイル.wav -│ └── 非登録ファイル1.txt -├── ... +登録ディレクトリ/ +├── filemap.json +├── dir_1/ +│ ├── 登録ファイル.png +│ ├── samples/ +│ │ └── 登録ファイル.wav +│ └── 非登録ファイル1.txt +└── dir_2/ + ├── 登録ファイル.png + ├── samples/ + │ └── 登録ファイル.wav + └── 非登録ファイル1.txt ``` #### filemap.json From 1281341452b6d4a5f607181b8147ee4fb50402d3 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 21 Jun 2024 07:37:30 +0900 Subject: [PATCH 73/82] =?UTF-8?q?FIX:=20=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E5=8C=BA=E5=88=87=E3=82=8A=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" | 2 -- 1 file changed, 2 deletions(-) diff --git "a/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" "b/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" index 6d7baa3c7..81aee09d6 100644 --- "a/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" +++ "b/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" @@ -21,8 +21,6 @@ ## filemap.json -### 仕様 - キーは登録するファイルパスを登録するディレクトリを基準にした相対パスです。 値は登録するファイルを一意に識別できるハッシュ等の文字列です。 From ae7e0f3beb2514308a8b45ae9eebc366bad057f7 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:32:45 +0900 Subject: [PATCH 74/82] =?UTF-8?q?DOC:=20=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...53\343\201\244\343\201\204\343\201\246.md" | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git "a/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" "b/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" index 81aee09d6..b33ad7592 100644 --- "a/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" +++ "b/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" @@ -1,27 +1,25 @@ # ResourceManagerの仕様について -## ResourceManager - -### 初期化 +一部のリソースファイルはURLとして返します。 +ファイルの内容が変わってもURLが同じ場合、キャッシュが働いて新しいリソースを取得できない可能性があります。 +これを防ぐためハッシュを使用してリソースの変更の度にURLを変更します。 -`create_filemap_if_not_exist`に`True`を渡すことによって登録するディレクトリに`filemap.json`が存在しないディレクトリを登録可能にします。 -これは開発時を想定したものです。 +ResourceManagerはファイルとハッシュを管理します。 +filemap.jsonは事前に生成したハッシュとファイルの関連付けを行います。 +generate_filemap.pyはfilemap.pyの作成を行います。 -### リソースの登録 - -`register_dir(resource_dir)`でディレクトリ内のリソースを登録します。 -登録されるリソースは`filemap.json`にあるものに限ります。 +## ResourceManager -`filemap.json`がない場合 -- `create_filemap_if_not_exist`が`False` -`ResourceManagerError`が発生します。 +初期化時に`create_filemap_if_not_exist`を`True`にすると`filemap.json`がないディレクトリの登録ができます。 +登録されるリソースは`filemap.json`にあるものに限ります。 +`filemap.json`がない場合ディレクトリ内のすべてのファイルが登録されます。 -- `create_filemap_if_not_exist`が`True` -ディレクトリ内のすべてのファイルが登録されます。 +細かい仕様はResourceManagerのドキュメントと実装を確認してください。 ## filemap.json -キーは登録するファイルパスを登録するディレクトリを基準にした相対パスです。 +キーは登録するファイルパスを登録するディレクトリを基準にした相対パスです。 +パス区切り文字は互換性のため`/`である必要があります。 値は登録するファイルを一意に識別できるハッシュ等の文字列です。 `generate_filemap.py`はsha256ハッシュを生成します。 From 73710d04878c1fbfd7eef763301fe32dd556e7d6 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:35:26 +0900 Subject: [PATCH 75/82] =?UTF-8?q?FIX:=20=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E5=90=8D=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\343\202\241\343\202\244\343\203\253URL\343\201\250filemap.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" => "docs/\343\203\225\343\202\241\343\202\244\343\203\253URL\343\201\250filemap.md" (100%) diff --git "a/docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" "b/docs/\343\203\225\343\202\241\343\202\244\343\203\253URL\343\201\250filemap.md" similarity index 100% rename from "docs/ResourceManager\343\201\256\344\273\225\346\247\230\343\201\253\343\201\244\343\201\204\343\201\246.md" rename to "docs/\343\203\225\343\202\241\343\202\244\343\203\253URL\343\201\250filemap.md" From 726e630efbf0bbbb8417a3843bd6c5947f41c2e2 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Sun, 23 Jun 2024 10:05:28 +0900 Subject: [PATCH 76/82] =?UTF-8?q?FIX:=20=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba --- test/unit/resource_manager/test_resource_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index af62c66f4..17cc765db 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -30,7 +30,7 @@ def _assert_resource(manager: ResourceManager, input_path: Path) -> None: def test_with_filemap() -> None: """ "filemap.json"があるディレクトリでのテスト - fimemapの生成方法 + (fimemapの生成コマンド) `python tools/generate_filemap.py --target_dir test/unit/resource_manager/with_filemap` """ manager = ResourceManager(False) From 555b6a627c706100e5629320ad2c646c825ef11d Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Sun, 23 Jun 2024 10:05:59 +0900 Subject: [PATCH 77/82] =?UTF-8?q?FIX:=20=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba --- test/unit/resource_manager/test_resource_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/resource_manager/test_resource_manager.py b/test/unit/resource_manager/test_resource_manager.py index 17cc765db..ca2a0dc71 100644 --- a/test/unit/resource_manager/test_resource_manager.py +++ b/test/unit/resource_manager/test_resource_manager.py @@ -48,7 +48,7 @@ def test_with_filemap() -> None: _assert_resource(manager, same_wav_path) # filemap.jsonに含まれないものはエラー - # NOTE: テキストは通常エンコードせずにJSONに含めるためResourceManagerでは管理しない + # NOTE: 通常、テキストはResourceManagerで管理しない txt_path = with_filemap_dir / "dummy.txt" with pytest.raises(ResourceManagerError): manager.resource_str(txt_path, "base64") From 9e7623cbfc987c5863d01bea4429844c4665ef29 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Sun, 23 Jun 2024 10:06:29 +0900 Subject: [PATCH 78/82] =?UTF-8?q?FIX:=20docstring=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba --- voicevox_engine/resource_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voicevox_engine/resource_manager.py b/voicevox_engine/resource_manager.py index 528265e56..b94e5443b 100644 --- a/voicevox_engine/resource_manager.py +++ b/voicevox_engine/resource_manager.py @@ -59,8 +59,8 @@ def resource_str( resource_path: Path, resource_format: Literal["base64", "hash"], ) -> str: - # NOTE: 意図しないパスのファイルの結果を返さないようにする """指定したリソースファイルのbase64文字列やハッシュ値を返す。""" + # NOTE: 意図しないパスのファイルの結果を返さないようにする filehash = self._path_to_hash.get(resource_path) if filehash is None: raise ResourceManagerError(f"{resource_path}がfilemapに登録されていません") From 0b351e901ddba95f3a28ed672619f886132ada13 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Sun, 23 Jun 2024 10:25:30 +0900 Subject: [PATCH 79/82] =?UTF-8?q?FIX:=20Depends=E3=81=AE=E5=86=85=E9=83=A8?= =?UTF-8?q?=E9=96=A2=E6=95=B0=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voicevox_engine/app/routers/speaker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voicevox_engine/app/routers/speaker.py b/voicevox_engine/app/routers/speaker.py index ec4a809f8..d81cdb102 100644 --- a/voicevox_engine/app/routers/speaker.py +++ b/voicevox_engine/app/routers/speaker.py @@ -16,7 +16,7 @@ ResourceFormat: TypeAlias = Literal["base64", "url"] -async def get_resource_baseurl(request: Request) -> str: +async def _get_resource_baseurl(request: Request) -> str: return f"{request.url.scheme}://{request.url.netloc}/{RESOURCE_ENDPOINT}" @@ -39,7 +39,7 @@ def speakers(core_version: str | SkipJsonSchema[None] = None) -> list[Speaker]: @router.get("/speaker_info") def speaker_info( - resource_baseurl: Annotated[str, Depends(get_resource_baseurl)], + resource_baseurl: Annotated[str, Depends(_get_resource_baseurl)], speaker_uuid: str, resource_format: ResourceFormat = "base64", core_version: str | SkipJsonSchema[None] = None, @@ -168,7 +168,7 @@ def singers(core_version: str | SkipJsonSchema[None] = None) -> list[Speaker]: @router.get("/singer_info") def singer_info( - resource_baseurl: Annotated[str, Depends(get_resource_baseurl)], + resource_baseurl: Annotated[str, Depends(_get_resource_baseurl)], speaker_uuid: str, resource_format: ResourceFormat = "base64", core_version: str | SkipJsonSchema[None] = None, From 62e3397154818fabbb2338b2102c28fcfb72b753 Mon Sep 17 00:00:00 2001 From: sabonerune <102559104+sabonerune@users.noreply.github.com> Date: Sun, 23 Jun 2024 10:45:15 +0900 Subject: [PATCH 80/82] =?UTF-8?q?FIX:=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E5=86=85=E9=83=A8=E9=96=A2=E6=95=B0=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/e2e/test_speakers.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/test/e2e/test_speakers.py b/test/e2e/test_speakers.py index a4af0ac22..6e7e4ec23 100644 --- a/test/e2e/test_speakers.py +++ b/test/e2e/test_speakers.py @@ -113,6 +113,9 @@ def test_歌手の情報を取得できる( def test_歌手の情報をURLで取得できる( client: TestClient, snapshot_json: SnapshotAssertion, snapshot: SnapshotAssertion ) -> None: + def assert_resource_url(url: str, name: str) -> None: + _assert_resource_url(client, snapshot, url, name) + singers = _speaker_list_adapter.validate_json(client.get("/singers").content) for singer in singers: singer_id = singer.speaker_uuid @@ -123,22 +126,13 @@ def test_歌手の情報をURLで取得できる( assert snapshot_json(name=singer_id) == response.json() speaker_info = SpeakerInfo.model_validate_json(response.content) - _assert_resource_url( - client, snapshot, speaker_info.portrait, f"{singer_id}_portrait" - ) + assert_resource_url(speaker_info.portrait, f"{singer_id}_portrait") for style in speaker_info.style_infos: - _assert_resource_url( - client, snapshot, style.icon, f"{singer_id}_{style.id}_icon" - ) + assert_resource_url(style.icon, f"{singer_id}_{style.id}_icon") if style.portrait is not None: - _assert_resource_url( - client, snapshot, style.portrait, f"{singer_id}_{style.id}_portrait" - ) + assert_resource_url(style.portrait, f"{singer_id}_{style.id}_portrait") for i, voice_sample in enumerate(style.voice_samples): - _assert_resource_url( - client, - snapshot, - voice_sample, - f"{singer_id}_{style.id}_voice_sample_{i}", + assert_resource_url( + voice_sample, f"{singer_id}_{style.id}_voice_sample_{i}" ) From d64ac469af34e261beab2c403bb12d8f6552abf8 Mon Sep 17 00:00:00 2001 From: Hiroshiba Kazuyuki Date: Sun, 23 Jun 2024 20:02:08 +0900 Subject: [PATCH 81/82] =?UTF-8?q?assert=5Fresource=5Furl=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\244\343\203\253URL\343\201\250filemap.md" | 72 ------------------- test/e2e/test_characters.py | 25 +++---- 2 files changed, 8 insertions(+), 89 deletions(-) delete mode 100644 "docs/\343\203\225\343\202\241\343\202\244\343\203\253URL\343\201\250filemap.md" diff --git "a/docs/\343\203\225\343\202\241\343\202\244\343\203\253URL\343\201\250filemap.md" "b/docs/\343\203\225\343\202\241\343\202\244\343\203\253URL\343\201\250filemap.md" deleted file mode 100644 index b33ad7592..000000000 --- "a/docs/\343\203\225\343\202\241\343\202\244\343\203\253URL\343\201\250filemap.md" +++ /dev/null @@ -1,72 +0,0 @@ -# ResourceManagerの仕様について - -一部のリソースファイルはURLとして返します。 -ファイルの内容が変わってもURLが同じ場合、キャッシュが働いて新しいリソースを取得できない可能性があります。 -これを防ぐためハッシュを使用してリソースの変更の度にURLを変更します。 - -ResourceManagerはファイルとハッシュを管理します。 -filemap.jsonは事前に生成したハッシュとファイルの関連付けを行います。 -generate_filemap.pyはfilemap.pyの作成を行います。 - -## ResourceManager - -初期化時に`create_filemap_if_not_exist`を`True`にすると`filemap.json`がないディレクトリの登録ができます。 -登録されるリソースは`filemap.json`にあるものに限ります。 -`filemap.json`がない場合ディレクトリ内のすべてのファイルが登録されます。 - -細かい仕様はResourceManagerのドキュメントと実装を確認してください。 - -## filemap.json - -キーは登録するファイルパスを登録するディレクトリを基準にした相対パスです。 -パス区切り文字は互換性のため`/`である必要があります。 - -値は登録するファイルを一意に識別できるハッシュ等の文字列です。 -`generate_filemap.py`はsha256ハッシュを生成します。 - -### 例 - -#### デイレクトリ構造 - -``` -登録ディレクトリ/ -├── filemap.json -├── dir_1/ -│ ├── 登録ファイル.png -│ ├── samples/ -│ │ └── 登録ファイル.wav -│ └── 非登録ファイル1.txt -└── dir_2/ - ├── 登録ファイル.png - ├── samples/ - │ └── 登録ファイル.wav - └── 非登録ファイル1.txt -``` - -#### filemap.json - -```json -{ - "dir_1/登録ファイル.png": "HASH-1", - "dir_1/samples/登録ファイル.wav": "HASH-2", - "dir_2/登録ファイル.png": "HASH-3", - "dir_2/samples/登録ファイル.wav": "HASH-4", -} -``` - -## generate_filemap.py - -`filemap.json`を生成するためのスクリプトです。 -デフォルトではpngファイルとwavファイルのみを登録します。 - -### 例 - -```bash -python tools/generate_filemap.py --target_dir resources/character_info -``` - -pngとwavに加えてjpgファイルを登録する例 -```bash -python tools/generate_filemap.py --target_dir resources/character_info \ - --target_suffix png --target_suffix wav --target_suffix jpg -``` diff --git a/test/e2e/test_characters.py b/test/e2e/test_characters.py index 6e7e4ec23..161cc3e05 100644 --- a/test/e2e/test_characters.py +++ b/test/e2e/test_characters.py @@ -55,6 +55,9 @@ def test_話者の情報を取得できる( def test_話者の情報をURLで取得できる( client: TestClient, snapshot_json: SnapshotAssertion, snapshot: SnapshotAssertion ) -> None: + def assert_resource_url(url: str, name: str) -> None: + _assert_resource_url(client, snapshot, url, name) + speakers = _speaker_list_adapter.validate_json(client.get("/speakers").content) for speaker in speakers: speaker_id = speaker.speaker_uuid @@ -65,27 +68,15 @@ def test_話者の情報をURLで取得できる( assert snapshot_json(name=speaker_id) == response.json() speaker_info = SpeakerInfo.model_validate_json(response.content) - _assert_resource_url( - client, snapshot, speaker_info.portrait, f"{speaker_id}_portrait" - ) + assert_resource_url(speaker_info.portrait, f"{speaker_id}_portrait") for style in speaker_info.style_infos: - _assert_resource_url( - client, snapshot, style.icon, f"{speaker_id}_{style.id}_icon" - ) + assert_resource_url(style.icon, f"{speaker_id}_{style.id}_icon") if style.portrait is not None: - _assert_resource_url( - client, - snapshot, - style.portrait, - f"{speaker_id}_{style.id}_portrait", - ) + assert_resource_url(style.portrait, f"{speaker_id}_{style.id}_portrait") for i, voice_sample in enumerate(style.voice_samples): - _assert_resource_url( - client, - snapshot, - voice_sample, - f"{speaker_id}_{style.id}_voice_sample_{i}", + assert_resource_url( + voice_sample, f"{speaker_id}_{style.id}_voice_sample_{i}" ) From bff011e098f25d258aef2bf8f2cc5e65b1dcaf19 Mon Sep 17 00:00:00 2001 From: Hiroshiba Kazuyuki Date: Sun, 23 Jun 2024 20:02:24 +0900 Subject: [PATCH 82/82] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=90=8D=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\253\343\201\256URL\343\201\250filemap.md" | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 "docs/\343\203\252\343\202\275\343\203\274\343\202\271\343\203\225\343\202\241\343\202\244\343\203\253\343\201\256URL\343\201\250filemap.md" diff --git "a/docs/\343\203\252\343\202\275\343\203\274\343\202\271\343\203\225\343\202\241\343\202\244\343\203\253\343\201\256URL\343\201\250filemap.md" "b/docs/\343\203\252\343\202\275\343\203\274\343\202\271\343\203\225\343\202\241\343\202\244\343\203\253\343\201\256URL\343\201\250filemap.md" new file mode 100644 index 000000000..ee743043d --- /dev/null +++ "b/docs/\343\203\252\343\202\275\343\203\274\343\202\271\343\203\225\343\202\241\343\202\244\343\203\253\343\201\256URL\343\201\250filemap.md" @@ -0,0 +1,72 @@ +# リソースファイルの URL の仕様について + +VOICEVOX ENGINE では一部のリソースファイルを URL として返します。 +リソースファイルを更新しても URL が同じ場合、キャッシュが働いて新しいリソースを取得できないことがあります。 +これを防ぐためにリソースファイルのハッシュ値を URL に含め、リソースの変更の度に URL が変わるようにしています。 + +ResourceManager はファイルとハッシュの対応を管理します。 +filemap.json はファイルとハッシュを予め対応付けたファイルです。 +generate_filemap.py は filemap.json の作成を行います。 + +## ResourceManager + +`filemap.json`にあるリソースファイルを登録できます。 +初期化時に`create_filemap_if_not_exist`を`True`にすると`filemap.json`がないディレクトリの登録ができます。 + +細かい仕様は ResourceManager のドキュメントと実装を確認してください。 + +## filemap.json + +`filemap.json`のキーは、登録するディレクトリからリソースファイルへの相対パスです。 +パス区切り文字は互換性のため`/`である必要があります。 + +値は登録するファイルを一意に識別できるハッシュ等の文字列です。 +`generate_filemap.py`は sha256 ハッシュを生成します。 + +### 例 + +#### デイレクトリ構造 + +``` +登録ディレクトリ/ +├── filemap.json +├── dir_1/ +│ ├── 登録ファイル.png +│ ├── samples/ +│ │ └── 登録ファイル.wav +│ └── 非登録ファイル1.txt +└── dir_2/ + ├── 登録ファイル.png + ├── samples/ + │ └── 登録ファイル.wav + └── 非登録ファイル1.txt +``` + +#### filemap.json + +```json +{ + "dir_1/登録ファイル.png": "HASH-1", + "dir_1/samples/登録ファイル.wav": "HASH-2", + "dir_2/登録ファイル.png": "HASH-3", + "dir_2/samples/登録ファイル.wav": "HASH-4" +} +``` + +## generate_filemap.py + +`filemap.json`を生成するためのスクリプトです。 +デフォルトでは png ファイルと wav ファイルのみを登録します。 + +### 例 + +```bash +python tools/generate_filemap.py --target_dir resources/character_info +``` + +png と wav に加えて jpg ファイルを登録する例 + +```bash +python tools/generate_filemap.py --target_dir resources/character_info \ + --target_suffix png --target_suffix wav --target_suffix jpg +```