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

エンジン側でいろいろ設定できるGUIの追加 #531

Merged
merged 17 commits into from
Dec 31, 2022
Merged
Changes from 15 commits
Commits
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -267,6 +267,19 @@ curl -s -X GET "localhost:50021/speaker_info?speaker_uuid=7ffcb7ce-00ec-4bdc-82c
この API は実験的機能であり、エンジン起動時に引数で`--enable_cancellable_synthesis`を指定しないと有効化されません。
音声合成に必要なパラメータは`/synthesis`と同様です。

### CORS設定

VOICEVOXではセキュリティ保護のため`localhost`・`127.0.0.1`・`app://`・Originなし以外のOriginからリクエストを受け入れないようになっています。
そのため、一部のサードパーティアプリからのレスポンスを受け取れない可能性があります。
これを回避する方法として、エンジンから設定できるUIを用意しています。

#### 設定方法

1. <http://127.0.0.1:50021/setting> にアクセスします。
2. 利用するアプリに合わせて設定を変更、追加してください。
3. 保存ボタンを押して、変更を確定してください。
4. 設定の適用にはエンジンの再起動が必要です。必要に応じて再起動をしてください。

## アップデート

エンジンディレクトリ内にあるファイルを全て消去し、新しいものに置き換えてください。
2 changes: 2 additions & 0 deletions default_setting.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allow_origin: null
cors_policy_mode: localapps
4 changes: 4 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -56,6 +56,10 @@ idna==3.3
# via
# anyio
# requests
jinja2==3.1.2
# via -r requirements.in
markupsafe==2.1.1
# via jinja2
nodeenv==1.6.0
# via pre-commit
numpy==1.20.0
4 changes: 4 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -74,6 +74,10 @@ iniconfig==1.1.1
# via pytest
isort==5.1.4
# via pysen
jinja2==3.1.2
# via -r requirements.in
markupsafe==2.1.1
# via jinja2
mccabe==0.6.1
# via flake8
mypy==0.790
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
numpy
fastapi
python-multipart
jinja2
uvicorn
aiofiles
soundfile
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -36,6 +36,10 @@ idna==3.3
# via
# anyio
# requests
jinja2==3.1.2
# via -r requirements.in
markupsafe==2.1.1
# via jinja2
numpy==1.20.0
# via
# -r requirements.in
91 changes: 81 additions & 10 deletions run.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@
import traceback
import zipfile
from distutils.version import LooseVersion
from enum import Enum
from functools import lru_cache
from io import TextIOWrapper
from pathlib import Path
@@ -20,10 +19,11 @@
import requests
import soundfile
import uvicorn
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi import FastAPI, Form, HTTPException, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.params import Query
from fastapi.responses import JSONResponse
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pydantic import ValidationError, conint
from starlette.background import BackgroundTask
from starlette.responses import FileResponse
@@ -51,6 +51,12 @@
)
from voicevox_engine.part_of_speech_data import MAX_PRIORITY, MIN_PRIORITY
from voicevox_engine.preset import Preset, PresetLoader
from voicevox_engine.setting import (
USER_SETTING_PATH,
CorsPolicyMode,
Setting,
SettingLoader,
)
from voicevox_engine.synthesis_engine import SynthesisEngineBase, make_synthesis_engines
from voicevox_engine.user_dict import (
apply_word,
@@ -68,11 +74,6 @@
)


class CorsPolicyMode(str, Enum):
all = "all"
localapps = "localapps"


def b64encode_str(s):
return base64.b64encode(s).decode("utf-8")

@@ -105,6 +106,7 @@ def set_output_log_utf8() -> None:
def generate_app(
synthesis_engines: Dict[str, SynthesisEngineBase],
latest_core_version: str,
setting_loader: SettingLoader,
root_dir: Optional[Path] = None,
cors_policy_mode: CorsPolicyMode = CorsPolicyMode.localapps,
allow_origin: Optional[List[str]] = None,
@@ -173,6 +175,8 @@ async def block_origin_middleware(request: Request, call_next):
root_dir / "engine_manifest.json", root_dir
)

setting_ui_template = Jinja2Templates(directory=engine_root() / "ui_template")

# キャッシュを有効化
# モジュール側でlru_cacheを指定するとキャッシュを制御しにくいため、HTTPサーバ側で指定する
# TODO: キャッシュを管理するモジュール側API・HTTP側APIを用意する
@@ -875,6 +879,51 @@ def supported_devices(
def engine_manifest():
return engine_manifest_loader.load_manifest()

@app.get("/setting", response_class=HTMLResponse)
def setting_get(request: Request):
settings = setting_loader.load_setting_file()

cors_policy_mode = settings.cors_policy_mode
allow_origin = settings.allow_origin

if allow_origin is None:
allow_origin = ""

return setting_ui_template.TemplateResponse(
"ui.html",
{
"request": request,
"cors_policy_mode": cors_policy_mode,
"allow_origin": allow_origin,
},
)

@app.post("/setting", response_class=HTMLResponse)
def setting_post(
request: Request,
cors_policy_mode: Optional[str] = Form(None), # noqa: B008
allow_origin: Optional[str] = Form(None), # noqa: B008
):
settings = Setting(
cors_policy_mode=cors_policy_mode,
allow_origin=allow_origin,
)

# 更新した設定へ上書き
setting_loader.dump_setting_file(settings)

if allow_origin is None:
allow_origin = ""

return setting_ui_template.TemplateResponse(
"ui.html",
{
"request": request,
"cors_policy_mode": cors_policy_mode,
"allow_origin": allow_origin,
},
)

return app


@@ -962,6 +1011,10 @@ def engine_manifest():
"--allow_origin", nargs="*", help="許可するオリジンを指定します。複数指定する場合は、直後にスペースで区切って追加できます。"
)

parser.add_argument(
"--setting_file", type=Path, default=USER_SETTING_PATH, help="設定ファイルを指定できます。"
)

args = parser.parse_args()

if args.output_log_utf8:
@@ -986,13 +1039,31 @@ def engine_manifest():
cancellable_engine = CancellableEngine(args)

root_dir = args.voicevox_dir if args.voicevox_dir is not None else engine_root()

setting_file_path = args.setting_file

setting_loader = SettingLoader(setting_file_path)

settings = setting_loader.load_setting_file()

cors_policy_mode = (
args.cors_policy_mode
if args.cors_policy_mode is not CorsPolicyMode.localapps
else settings.cors_policy_mode
)

allow_origin = (
args.allow_origin if args.allow_origin is not None else settings.allow_origin
)

uvicorn.run(
generate_app(
synthesis_engines,
latest_core_version,
setting_loader,
root_dir=root_dir,
cors_policy_mode=args.cors_policy_mode,
allow_origin=args.allow_origin,
cors_policy_mode=cors_policy_mode,
allow_origin=allow_origin,
),
host=args.host,
port=args.port,
2 changes: 2 additions & 0 deletions run.spec
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ datas = [
('default.csv', '.'),
('licenses.json', '.'),
('presets.yaml', '.'),
('default_setting.yml', '.'),
('ui_template', 'ui_template'),
]
datas += collect_data_files('pyopenjtalk')

118 changes: 118 additions & 0 deletions ui_template/ui.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>VOICEVOX Engine 設定</title>
<link
rel="shortcut icon"
href="https://voicevox.hiroshiba.jp/favicon-32x32.png"
/>

<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"
></script>
</head>

<body>
<div class="container p-3">
<form method="post">
<div class="alert alert-warning" role="alert">
設定の変更の更新にはエンジンの再起動が必要です。
</div>

<div class="mb-3">
<label class="form-label">CORS Policy Mode</label>
<select
class="form-select"
aria-label="cors_policy_mode"
name="cors_policy_mode"
>
<option selected>{{ cors_policy_mode }}</option>
<option value="localapps">localapps</option>
<option value="all">all</option>
</select>
<div class="form-text">
<p class="mb-1">
allまたはlocalappsを指定。allはすべてを許可します。
</p>
<p class="mb-1">
localappsはオリジン間リソース共有ポリシーを、app://.とlocalhost関連に限定します。
</p>
<p>
その他のオリジンはallow_originオプションで追加できます。デフォルトはlocalapps。
</p>
</div>
</div>

<div class="mb-3">
<label class="form-label">Allow Origin</label>
<input
class="form-control"
type="text"
name="allow_origin"
value="{{ allow_origin }}"
/>
<div class="form-text">
許可するオリジンを指定します。複数指定する場合は、直後にスペースで区切って追加できます。
</div>
</div>

<div
class="modal fade"
id="submitModal"
tabindex="-1"
aria-labelledby="submitModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="submitModalLabel">
設定の保存
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
設定をを保存します。よろしいですか?
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
キャンセル
</button>
<button type="submit" class="btn btn-primary">
保存
</button>
</div>
</div>
</div>
</div>

<button
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#submitModal"
>
保存
</button>
</form>
</div>
</body>
</html>
21 changes: 21 additions & 0 deletions voicevox_engine/setting/Setting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from enum import Enum
from typing import Optional

from pydantic import BaseModel, Field


class CorsPolicyMode(str, Enum):
all = "all"
localapps = "localapps"


class Setting(BaseModel):
"""
エンジンの設定情報
"""

cors_policy_mode: CorsPolicyMode = Field(title="リソース共有ポリシー")
allow_origin: Optional[str] = Field(title="許可するオリジン")

class Config:
use_enum_values = True
Loading