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

ユーザー辞書のインポート、エクスポートをUIに追加 #676

Merged
merged 13 commits into from
Dec 7, 2023

Conversation

My-MC
Copy link
Contributor

@My-MC My-MC commented Apr 24, 2023

内容

ユーザー辞書のインポート、エクスポートをUIに追加します。

UIはSettingのUIに追加する形にして、JavaScriptはかかないようにしました。

またread_dict関数のパーズ処理を違う関数に切り出し、利用するようにしました。
理由といたしましては、物理ファイルのようにpathlibが使えないためデータを渡してあげる感じになりました。

実際にファイルのエクスポート、インポート、overrideも行えることを確認しました。

関連 Issue

スクリーンショット・動画など

image

その他

遅くなってしまい申し訳ございませんでした。

@My-MC My-MC requested a review from a team as a code owner April 24, 2023 14:01
@My-MC My-MC requested review from y-chan and removed request for a team April 24, 2023 14:01
@github-actions
Copy link

github-actions bot commented Apr 24, 2023

Coverage Result

Resultを開く
Name Stmts Miss Cover
voicevox_engine/init.py 2 1 coverage-50%
voicevox_engine/acoustic_feature_extractor.py 75 0 coverage-100%
voicevox_engine/dev/synthesis_engine/init.py 3 1 coverage-67%
voicevox_engine/dev/synthesis_engine/mock.py 36 2 coverage-94%
voicevox_engine/downloadable_library.py 93 5 coverage-95%
voicevox_engine/full_context_label.py 162 3 coverage-98%
voicevox_engine/kana_parser.py 86 1 coverage-99%
voicevox_engine/metas/Metas.py 33 0 coverage-100%
voicevox_engine/metas/MetasStore.py 29 14 coverage-52%
voicevox_engine/metas/init.py 2 0 coverage-100%
voicevox_engine/model.py 160 9 coverage-94%
voicevox_engine/mora_list.py 5 0 coverage-100%
voicevox_engine/part_of_speech_data.py 5 0 coverage-100%
voicevox_engine/preset/Preset.py 12 0 coverage-100%
voicevox_engine/preset/PresetError.py 3 1 coverage-67%
voicevox_engine/preset/PresetManager.py 81 2 coverage-98%
voicevox_engine/preset/init.py 4 0 coverage-100%
voicevox_engine/setting/Setting.py 11 0 coverage-100%
voicevox_engine/setting/SettingLoader.py 19 0 coverage-100%
voicevox_engine/setting/init.py 3 0 coverage-100%
voicevox_engine/synthesis_engine/init.py 5 0 coverage-100%
voicevox_engine/synthesis_engine/core_wrapper.py 201 159 coverage-21%
voicevox_engine/synthesis_engine/make_synthesis_engines.py 59 51 coverage-14%
voicevox_engine/synthesis_engine/synthesis_engine.py 130 11 coverage-92%
voicevox_engine/synthesis_engine/synthesis_engine_base.py 67 9 coverage-87%
voicevox_engine/user_dict.py 144 11 coverage-92%
voicevox_engine/utility/init.py 5 0 coverage-100%
voicevox_engine/utility/connect_base64_waves.py 37 0 coverage-100%
voicevox_engine/utility/core_version_utility.py 9 2 coverage-78%
voicevox_engine/utility/mutex_utility.py 11 1 coverage-91%
voicevox_engine/utility/path_utility.py 26 8 coverage-69%
TOTAL 1518 291 coverage-81%

@My-MC
Copy link
Contributor Author

My-MC commented Apr 30, 2023

テストの追加の必要がないことと、そのほかの不具合がないことが自分が見た中ではないことを確認しました。

Copy link
Member

@takana-v takana-v left a comment

Choose a reason for hiding this comment

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

遅くなって申し訳ありません。
コメントを追加しておきました。

voicevox_engine/user_dict.py Outdated Show resolved Hide resolved
ui_template/ui.html Outdated Show resolved Hide resolved
run.py Outdated Show resolved Hide resolved
@takana-v
Copy link
Member

takana-v commented Apr 30, 2023

少し考えてみたのですが、辞書のコストが/user_dictではpriority表記になっており、cost表記になっている実ファイルを使った方が良い可能性があるので一旦レビューを取り下げました。

/user_dictを使った方がコード的にはかなりスッキリするのですが、
(user_dict.pyの変更と/download_dictの追加が不要になる)
もし品詞ごとに設定しているpriorityに紐づくcostが大きく変更された場合、大きな影響を受けることになります。
/download_dictを使う場合は、costが近いpriorityに補正されるので、そこまで大きな影響は受けません。

個人的にはpriorityに紐づくcostの変更を行う可能性はほぼ無いと思うため、/user_dictを使う形式でも問題ないかなと思いましたがちょっと他の方の意見も伺いたいです。 @Hiroshiba

@My-MC
Copy link
Contributor Author

My-MC commented Apr 30, 2023

@takana-v 様 自分の方で検証を行って確かめました。まず/user_dictの返すjsonをそのままdownloadして、それを読み込ませたところ、おっしゃるとおりkeyエラーが発生し、そもそもの関数の書き換えが必要になるような形になる感じでした。
以下ログです。

Traceback (most recent call last):
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 373, in run_asgi
    result = await app(self.scope, self.receive, self.send)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\fastapi\applications.py", line 208, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\applications.py", line 112, in 
__call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
    raise exc
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\middleware\base.py", line 53, in __call__
    async with anyio.create_task_group() as task_group:
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\anyio\_backends\_asyncio.py", line 662, in __aexit__
    raise exceptions[0]
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\middleware\base.py", line 30, in coro
    await self.app(scope, request.receive, send_stream.send)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\middleware\cors.py", line 92, in __call__
    await self.simple_response(scope, receive, send, request_headers=headers)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\middleware\cors.py", line 147, 
in simple_response
    await self.app(scope, receive, send)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\exceptions.py", line 82, in __call__
    raise exc
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\routing.py", line 656, in __call__
    await route.handle(scope, receive, send)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\routing.py", line 259, in handle
    await self.app(scope, receive, send)
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\routing.py", line 61, in app   
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\fastapi\routing.py", line 226, in app    
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\fastapi\routing.py", line 161, in run_endpoint_function
    return await run_in_threadpool(dependant.call, **values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\starlette\concurrency.py", line 39, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\anyio\to_thread.py", line 31, in run_sync    return await get_asynclib().run_sync_in_worker_thread(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\anyio\_backends\_asyncio.py", line 937, in run_sync_in_worker_thread
    return await future
           ^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\.venv\Lib\site-packages\anyio\_backends\_asyncio.py", line 867, in run
    result = context.run(func, *args)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\run.py", line 1105, in setting_post
    parse_dict(user_dictionary_file.file), override=allow_override
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user_name\Desktop\python\voicevox_engine\voicevox_engine\user_dict.py", line 129, in parse_dict
    word["priority"] = cost2priority(word["context_id"], word["cost"])
                                                         ~~~~^^^^^^^^
KeyError: 'cost'

Co-authored-by: takana-v <44311840+takana-v@users.noreply.github.com>
@takana-v
Copy link
Member

テストが落ちているみたいですね。
coverallsの方の問題かもしれません。
lemurheavy/coveralls-public#1710

@Hiroshiba
Copy link
Member

僕も @takana-v さんと同じ意見で、既存の/user_dict/import_user_dictを使い回す形にすると良いかなと思いました!!

理由は経路の統合で、辞書のimport/exportはこの経路だけにしておくとメンテナンス性が上がりそうだなと思ったためです。
(今のところはpriorityとcostの変換だけですが)

ちなみに/user_dictこれ/import_user_dictこれです!
どう利用すればよいかわからない等あればなんでも聞いていただければ!! @My-MC

@My-MC
Copy link
Contributor Author

My-MC commented May 3, 2023

@Hiroshiba/import_user_dictを使うとなると、HTML内にJavaScriptを記述する必要ができて、メインのPython以外のスクリプトが増えることになり、初学者の方などの参入が難しくなります。僕としてはPython以外の言語をなるべく書かない方針のほうが良いと思うのですがどうでしょうか。

@takana-v
Copy link
Member

takana-v commented May 3, 2023

@My-MC
これら2つのsuggestionのように記述することで、Javascriptの記述無しで/user_dict/import_user_dictを使いまわす形にできると思います。
(もしこの2つのsuggestionを適用してもエラーが出る場合は教えてください!)
#676 (comment)
#676 (comment)

@My-MC
Copy link
Contributor Author

My-MC commented May 3, 2023

あ、/import_user_dictを使うということではなくDict[str, UserDictWord]の型を使うということでよろしいでしょうか。

@takana-v
Copy link
Member

takana-v commented May 3, 2023

あ、ちょっと記述ミスをしていました。
使うのは/import_user_dictではなくimport_user_dict()ですね。(直接関数を実行するので)
/download_dictではなく/user_dictを用いてDict[str, UserDictWord]のデータを取得し、import_user_dict()関数を用いてインポートする、という認識です。

@My-MC
Copy link
Contributor Author

My-MC commented May 3, 2023

なるほどわかりました。もしかしたら認識の行き違いが発生してるかもしれないのですが、 @Hiroshiba 様はどうですか。

@takana-v
Copy link
Member

takana-v commented May 3, 2023

先ほどの@My-MC さんのコメントで気付きましたが、私の方に認識ミスが発生してましたね...申し訳ありません。
私の意見としては
/download_dictではなく/user_dictを使いたい
/import_user_dictに関しては、Javascriptを書く必要がありコードが複雑になりそうなら、内部の関数であるimport_user_dict()を実行する形式でも問題ない
import_user_dict()を使うのを一カ所だけにすることによるメンテナンスコストの削減幅よりもJavascriptのコードのメンテナンスコストの方が大きいと感じます)
といった感じです。
ちょっとお手数おかけしますが、メンテナ判断をお願いしたいです。 @Hiroshiba

@Hiroshiba
Copy link
Member

Hiroshiba commented May 3, 2023

いくつか観点がありそうなのでまとめてみます。

user_dictdownload_dictで辞書のimport/export経路が違う
→少なくともuser_dictdownload_dictは降ってくる辞書の形式が違うので統一すべきだと思います

・JavaScriptを書かないといけなくなる
→ user_dictimport_user_dictを使う場合も、JS書かなくて良いはず・・・?(jsonをファイルとしてformで投げれる気がします)
まあそもそも、辞書がimportできたのか成否をユーザーに伝えてあげるコードを足したほうが良さそうだな~とちょっと思いました。

user_dictimport_user_dictを使い回すか、download_dictを足してsetting_postに機能を増やすか
→一般的に、同じ機能を持つAPIを増やすことはメンテナンスが大変になるので避けれるととても嬉しいです。
片方に機能実装するときもう片方にも実装が必要、それらが同じ挙動をすることの保証が必要、ドキュメントも増える、なぜ2つあるのか開発者が困惑する、仮に不要になっても消せない、などなど。
一方でJSコードを増やすのは、そのコードをメンテナンスすればOKなので、実はこちらのがだいぶ安い感じです・・・!

@My-MC
Copy link
Contributor Author

My-MC commented May 3, 2023

・JavaScriptを書かないといけなくなる
→ user_dictimport_user_dictを使う場合も、JS書かなくて良いはず・・・?(jsonをファイルとしてformで投げれる気がします)
まあそもそも、辞書がimportできたのか成否をユーザーに伝えてあげるコードを足したほうが良さそうだな~とちょっと思いました。

これなんですが使用上formで違うURL(エンドポイント)を使う場合、そのページにリダイレクトするのと、HTMLのコードが繰り返しの多い見ずらいコードになってしまいます。またそもそもbodyにそもままファイルを送っても処理ができません。実際にどちらも検証をし、確認しました。

ですので個人的には/import_user_dictは使わないほうがいいのかなと思いました。しかし、 @takana-v さんと @Hiroshiba でJavaScriptのメンテナンスコストの見積もりの違いがあるのも含めてissueを一回立ててみてもいいのかなと思いました。

@Hiroshiba
Copy link
Member

Hiroshiba commented May 4, 2023

検証してみました。たしかにHTMLのformだけで/import_user_dictを使うことはできなさそうでした。(POSTリクエスト、難しいですね・・・。)

良い方法があれば無理に/import_user_dictAPIを使わなくてもいいかなと思います。
ただ、もしsetting_postでimportする場合、辞書importがどうしても設定保存と同タイミングになるので、「インポートする辞書を保存する」のかと勘違いされるUIになりがちだろうなと思いました。

実際のUI

image

内部的には保存ボタンを押した時点で辞書を更新しているのですが、ユーザーの方が「エンジン起動時に一緒にインポートする辞書を保存した」のだと勘違いしちゃいそうだなと。
「インポートする」というボタンがあって「辞書を正常にインポートしました」と結果が表示されれば、勘違いしちゃうユーザーを減らせそうかもです。

@My-MC
Copy link
Contributor Author

My-MC commented May 14, 2023

ユーザ辞書の保存時にメッセージのトーストがでるようにしました。

image

image

画像のキャプチャー方法の都合上トーストが変なところに出てきてますが、通常は右下に表示されます。

Copy link
Member

@Hiroshiba Hiroshiba left a comment

Choose a reason for hiding this comment

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

トースト良いですね!!

まだちょっとユーザーに混乱されそうなUIになっちゃっていそうなのでコメントしてみました。

run.py Outdated Show resolved Hide resolved
run.py Outdated Show resolved Hide resolved
@My-MC
Copy link
Contributor Author

My-MC commented Aug 13, 2023

今年に入り新しい環境でご助言をいただいてから時間が開いてしまい申し訳ありません。
辞書のimportをJavaScriptを使い/import_user_dictエンドポイントを使用するように変更を行いました。まだトーストの対応ができていませんがJavaScriptのコードの確認をお願いします。

@Hiroshiba
Copy link
Member

@My-MC コード読ませていただきました! すごくいい感じだと思います!!!
続きをぜひよろしくお願いします・・・!!

@My-MC
Copy link
Contributor Author

My-MC commented Aug 19, 2023

辞書の保存のModalにも対応しました。機能の実装はこれですべてだと思います。レビューお願いします。

Copy link
Member

@Hiroshiba Hiroshiba left a comment

Choose a reason for hiding this comment

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

LGTM!!

ちょっとUIの細かいところだけ勝手に調整してコミットさせていただきました!!
ブートストラップのトーストの使い方の正しい方法は多分こっちだと思うのでそれも変えさせていただきました!

@Hiroshiba
Copy link
Member

@takana-v さんか @y-chan さん、レビューお願いできると心強いです 🙇

@Hiroshiba
Copy link
Member

設定UIが今生HTMLになってるので、Vuejsに変えられればと思ってissue立ててみました!

@Hiroshiba
Copy link
Member

多分問題ないと思うのでマージさせていただきます!!
実装ありがとうございました!!

@Hiroshiba Hiroshiba merged commit f8d6acf into VOICEVOX:master Dec 7, 2023
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants