Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add: ライブラリのダウンロードAPIを実装 #616

Merged
merged 26 commits into from
Feb 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
929436c
(PR用:空コミット)
sevenc-nanashi Feb 6, 2023
cd74b02
Add: Engine manifestにmanage_libraryを追加
sevenc-nanashi Feb 6, 2023
ed5c79d
Add: /downloadable_librariesを外部リクエストから取得するように
sevenc-nanashi Feb 6, 2023
cb81ca4
Fix: 何故か消えていたところを復元
sevenc-nanashi Feb 7, 2023
a8593f9
Add: インストールを実装
sevenc-nanashi Feb 7, 2023
87bf6b6
Add: gitignoreに追記
sevenc-nanashi Feb 7, 2023
fe776fd
Code: isortを実行
sevenc-nanashi Feb 7, 2023
07f2cc1
Change: ステータスコードを404に
sevenc-nanashi Feb 17, 2023
14899d3
Change: run_in_executorで非同期に
sevenc-nanashi Feb 17, 2023
5dfddf7
Change: for-elseを使うように
sevenc-nanashi Feb 17, 2023
4a960c8
Fix: エンコーディングを指定するように
sevenc-nanashi Feb 17, 2023
8b29cd3
Code: isortを実行
sevenc-nanashi Feb 17, 2023
905abae
Change: manage_libraryを無効化
sevenc-nanashi Feb 19, 2023
3a8c069
Delete: engine_manifestからキーを削除
sevenc-nanashi Feb 19, 2023
cd0bb43
Change: exist_okを使うように
sevenc-nanashi Feb 19, 2023
6ba83e4
Change: エラーメッセージを変更
sevenc-nanashi Feb 19, 2023
6c8a830
Change: exist_okを使うように
sevenc-nanashi Feb 19, 2023
0e3dd75
Change: ライブラリ -> 音声ラブラリ
sevenc-nanashi Feb 19, 2023
9afa93c
Change: INFO_FILEの名前を変更
sevenc-nanashi Feb 19, 2023
c41edec
Update: engine_manifest.jsonを更新
sevenc-nanashi Feb 19, 2023
99f25b6
Delete: 不要な初期化を削除
sevenc-nanashi Feb 19, 2023
14be9fd
Change: ファイルから読み込むように
sevenc-nanashi Feb 23, 2023
0f5badd
Change: バージョンを変更
sevenc-nanashi Feb 23, 2023
49e7b96
Code: flake8に引っかからないように
sevenc-nanashi Feb 24, 2023
59ad8ee
Change: インストール先を変更
sevenc-nanashi Feb 24, 2023
2ecf178
Delete: 不要なroot_dirを削除
sevenc-nanashi Feb 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions engine_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
"terms_of_service": "engine_manifest_assets/terms_of_service.md",
"update_infos": "engine_manifest_assets/update_infos.json",
"dependency_licenses": "engine_manifest_assets/dependency_licenses.json",
"downloadable_libraries_path": null,
"downloadable_libraries_url": null,
"supported_features": {
"adjust_mora_pitch": {
"type": "bool",
Expand Down Expand Up @@ -54,6 +52,11 @@
"type": "bool",
"value": true,
"name": "2人の話者でモーフィングした音声を合成"
},
"manage_library": {
"type": "bool",
"value": true,
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
"name": "音声ライブラリのインストール・アンインストール"
}
}
}
75 changes: 37 additions & 38 deletions engine_manifest_assets/downloadable_libraries.json
Original file line number Diff line number Diff line change
@@ -1,45 +1,44 @@
[
{
"download_url": "",
"bytes": "1000",
"speaker": {
"name": "dummy1",
"speaker_uuid": "dummy1",
"styles": [
{
"name": "style1",
"id": 0
"name": "Dummy Library",
"uuid": "2bb8bccf-1c3f-4bc9-959a-f388e37af3ad",
"version": "0.0.1",
"download_url": "https://github.com/VOICEVOX/voicevox_engine/archive/d7cf31c058bc83e1abf8e14d4231a06409c4cc2d.zip",
"bytes": 1000,
"speakers": [
{
"speaker": {
"name": "dummy1",
"speaker_uuid": "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff",
"styles": [
{
"name": "style1",
"id": 0
},
{
"name": "style2",
"id": 2
}
],
"version": "0.0.1"
},
{
"name": "style2",
"id": 1
}
],
"version": "0.0.1"
},
"speaker_info": {
"policy": "",
"portrait": "",
"style_infos": [
{
"id": 0,
"icon": "",
"voice_samples": [
"",
"",
""
]
},
{
"id": 1,
"icon": "",
"voice_samples": [
"",
"",
""
"speaker_info": {
"policy": "",
"portrait": "",
"style_infos": [
{
"id": 0,
"icon": "",
"voice_samples": ["", "", ""]
},
{
"id": 2,
"icon": "",
"voice_samples": ["", "", ""]
}
]
}
]
}
}
]
}
]
80 changes: 54 additions & 26 deletions run.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import argparse

# import asyncio
import asyncio
import base64
import json
import multiprocessing
Expand All @@ -11,12 +10,11 @@
import zipfile
from distutils.version import LooseVersion
from functools import lru_cache
from io import TextIOWrapper
from io import BytesIO, TextIOWrapper
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryFile
from typing import Dict, List, Optional

import requests
import soundfile
import uvicorn
from fastapi import FastAPI, Form, HTTPException, Query, Request, Response
Expand All @@ -29,6 +27,7 @@

from voicevox_engine import __version__
from voicevox_engine.cancellable_engine import CancellableEngine
from voicevox_engine.downloadable_library import LibraryManager
from voicevox_engine.engine_manifest import EngineManifestLoader
from voicevox_engine.engine_manifest.EngineManifest import EngineManifest
from voicevox_engine.kana_parser import create_kana, parse_kana
Expand Down Expand Up @@ -77,6 +76,7 @@
connect_base64_waves,
delete_file,
engine_root,
get_save_dir,
)


Expand Down Expand Up @@ -180,6 +180,7 @@ async def block_origin_middleware(request: Request, call_next):
engine_manifest_loader = EngineManifestLoader(
root_dir / "engine_manifest.json", root_dir
)
library_manager = LibraryManager(get_save_dir() / "installed_libraries")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここ将来的にはVVPPのvvpp-enginesと合わせたいですね!
vvlibとなるなら、そういう感じに。


metas_store = MetasStore(root_dir / "speaker_info")

Expand Down Expand Up @@ -799,36 +800,63 @@ def speaker_info(speaker_uuid: str, core_version: Optional[str] = None):
@app.get(
"/downloadable_libraries",
response_model=List[DownloadableLibrary],
tags=["その他"],
tags=["音声ライブラリ管理"],
)
def downloadable_libraries():
"""
ダウンロード可能なモデル情報を返します
ダウンロード可能な音声ライブラリの情報を返します

Returns
-------
ret_data: List[DownloadableLibrary]
"""
try:
manifest = engine_manifest_loader.load_manifest()
# APIからダウンロード可能な音声ライブラリを取得する場合
if manifest.downloadable_libraries_url:
response = requests.get(manifest.downloadable_libraries_url, timeout=60)
ret_data: List[DownloadableLibrary] = [
DownloadableLibrary(**d) for d in response.json()
]
# ローカルのファイルからダウンロード可能な音声ライブラリを取得する場合
elif manifest.downloadable_libraries_path:
with open(manifest.downloadable_libraries_path) as f:
ret_data: List[DownloadableLibrary] = [
DownloadableLibrary(**d) for d in json.load(f)
]
else:
raise Exception
except Exception:
traceback.print_exc()
raise HTTPException(status_code=422, detail="ダウンロード可能な音声ライブラリの取得に失敗しました。")
return ret_data
manifest = engine_manifest_loader.load_manifest()
if not manifest.supported_features.manage_library:
raise HTTPException(status_code=404, detail="この機能は実装されていません")
return library_manager.downloadable_libraries()
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved

@app.get(
"/installed_libraries",
response_model=List[DownloadableLibrary],
tags=["音声ライブラリ管理"],
)
def installed_libraries():
"""
インストールした音声ライブラリの情報を返します。

Returns
-------
ret_data: List[DownloadableLibrary]
"""
manifest = engine_manifest_loader.load_manifest()
if not manifest.supported_features.manage_library:
raise HTTPException(status_code=404, detail="この機能は実装されていません")
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
return library_manager.installed_libraries()

@app.post(
"/install_library/{library_uuid}",
status_code=204,
tags=["音声ライブラリ管理"],
)
async def install_library(library_uuid: str, request: Request):
"""
音声ライブラリをインストールします。
音声ライブラリのZIPファイルをリクエストボディとして送信してください。

Parameters
----------
library_uuid: str
音声ライブラリのID
"""
manifest = engine_manifest_loader.load_manifest()
if not manifest.supported_features.manage_library:
raise HTTPException(status_code=404, detail="この機能は実装されていません")
archive = BytesIO(await request.body())
loop = asyncio.get_event_loop()
await loop.run_in_executor(
None, library_manager.install_library, library_uuid, archive
)
return Response(status_code=204)

@app.post("/initialize_speaker", status_code=204, tags=["その他"])
def initialize_speaker(
Expand Down
86 changes: 86 additions & 0 deletions voicevox_engine/downloadable_library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import base64
import json
import zipfile
from io import BytesIO
from pathlib import Path
from typing import List

from fastapi import HTTPException

from voicevox_engine.model import DownloadableLibrary

__all__ = ["LibraryManager"]

INFO_FILE = "metas.json"


class LibraryManager:
def __init__(self, library_root_dir: Path):
self.library_root_dir = library_root_dir
self.library_root_dir.mkdir(exist_ok=True)

def downloadable_libraries(self):
# == ダウンロード情報をネットワーク上から取得する場合
# url = "https://example.com/downloadable_libraries.json"
# response = requests.get(url)
# return list(map(DownloadableLibrary.parse_obj, response.json()))

# == ダウンロード情報をjsonファイルから取得する場合
# with open(
# self.root_dir / "engine_manifest_assets" / "downloadable_libraries.json",
# encoding="utf-8",
# ) as f:
# return list(map(DownloadableLibrary.parse_obj, json.load(f)))

# ダミーとして、speaker_infoのアセットを読み込む
with open(
"./engine_manifest_assets/downloadable_libraries.json",
encoding="utf-8",
) as f:
libraries = json.load(f)
speaker_info = libraries[0]["speakers"][0]["speaker_info"]
mock_root_dir = Path("./speaker_info/7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff")
speaker_info["policy"] = (mock_root_dir / "policy.md").read_text()
speaker_info["portrait"] = base64.b64encode(
(mock_root_dir / "portrait.png").read_bytes()
)
for style_info in speaker_info["style_infos"]:
style_id = style_info["id"]
style_info["icon"] = base64.b64encode(
(mock_root_dir / "icons" / f"{style_id}.png").read_bytes()
)
style_info["voice_samples"] = [
base64.b64encode(
(
mock_root_dir / "voice_samples" / f"{style_id}_{i:0>3}.wav"
).read_bytes()
)
for i in range(1, 4)
]
return list(map(DownloadableLibrary.parse_obj, libraries))

def installed_libraries(self) -> List[DownloadableLibrary]:
library = []
for library_dir in self.library_root_dir.iterdir():
if library_dir.is_dir():
with open(library_dir / INFO_FILE, encoding="utf-8") as f:
library.append(json.load(f))
return library

def install_library(self, library_id: str, file: BytesIO):
for downloadable_library in self.downloadable_libraries():
if downloadable_library.uuid == library_id:
library_info = downloadable_library.dict()
break
else:
raise HTTPException(status_code=404, detail="指定された音声ライブラリが見つかりません。")
library_dir = self.library_root_dir / library_id
library_dir.mkdir(exist_ok=True)
with open(library_dir / INFO_FILE, "w", encoding="utf-8") as f:
json.dump(library_info, f, indent=4, ensure_ascii=False)
with zipfile.ZipFile(file) as zf:
if zf.testzip() is not None:
raise HTTPException(status_code=422, detail="不正なZIPファイルです。")

zf.extractall(library_dir)
return library_dir
7 changes: 1 addition & 6 deletions voicevox_engine/engine_manifest/EngineManifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class SupportedFeatures(BaseModel):
adjust_volume_scale: bool = Field(title="全体の音量の調整")
interrogative_upspeak: bool = Field(title="疑問文の自動調整")
synthesis_morphing: bool = Field(title="2人の話者でモーフィングした音声を合成")
manage_library: bool = Field(title="音声ライブラリのインストール・アンインストール")


class EngineManifest(BaseModel):
Expand All @@ -54,10 +55,4 @@ class EngineManifest(BaseModel):
terms_of_service: str = Field(title="エンジンの利用規約")
update_infos: List[UpdateInfo] = Field(title="エンジンのアップデート情報")
dependency_licenses: List[LicenseInfo] = Field(title="依存関係のライセンス情報")
downloadable_libraries_path: Optional[str] = Field(
title="ダウンロード可能な音声ライブラリ情報を取得するためのローカルjsonパス"
)
downloadable_libraries_url: Optional[str] = Field(
title="ダウンロード可能な音声ライブラリ情報を取得するためのAPIのURL"
)
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
supported_features: SupportedFeatures = Field(title="エンジンが持つ機能")
17 changes: 14 additions & 3 deletions voicevox_engine/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,26 @@ def __init__(self, speaker: int, *args: object, **kywrds: object) -> None:
super().__init__(f"speaker {speaker} is not found.", *args, **kywrds)


class LibrarySpeaker(BaseModel):
"""
音声ライブラリに含まれる話者の情報
"""

speaker: Speaker = Field(title="話者情報")
speaker_info: SpeakerInfo = Field(title="話者の追加情報")


class DownloadableLibrary(BaseModel):
"""
ダウンロード可能な音声ライブラリの情報(最新情報をwebで取得することを考慮して、ローカルの情報はない)
ダウンロード可能な音声ライブラリの情報
"""

name: str = Field(title="音声ライブラリの名前")
uuid: str = Field(title="音声ライブラリのUUID")
version: str = Field(title="音声ライブラリのバージョン")
download_url: str = Field(title="音声ライブラリのダウンロードURL")
bytes: int = Field(title="音声ライブラリのバイト数")
speaker: Speaker = Field(title="話者情報")
speaker_info: SpeakerInfo = Field(title="話者の追加情報")
speakers: List[LibrarySpeaker] = Field(title="音声ライブラリに含まれる話者のリスト")


USER_DICT_MIN_PRIORITY = 0
Expand Down